summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc.js6
-rw-r--r--.eslintignore1
-rw-r--r--.gitignore4
-rw-r--r--.gitlab-ci.yml188
-rw-r--r--.gitlab/CODEOWNERS (renamed from .gitlab/CODEOWNERS.disabled)0
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md2
-rw-r--r--CHANGELOG.md130
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile20
-rw-r--r--Gemfile.lock27
-rw-r--r--Gemfile.rails4.lock47
-rw-r--r--app/assets/javascripts/api.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js3
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js7
-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.vue14
-rw-r--r--app/assets/javascripts/diffs/components/app.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue13
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue21
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue9
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue21
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue20
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue17
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue43
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue29
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue14
-rw-r--r--app/assets/javascripts/diffs/store/actions.js39
-rw-r--r--app/assets/javascripts/diffs/store/getters.js34
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js45
-rw-r--r--app/assets/javascripts/diffs/store/utils.js13
-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/filtered_search/issuable_filtered_search_token_keys.js7
-rw-r--r--app/assets/javascripts/groups_select.js7
-rw-r--r--app/assets/javascripts/ide/components/panes/right.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/ide/services/index.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js12
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue10
-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/url_utility.js38
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js1
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue28
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js34
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue258
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue87
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue28
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue12
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue398
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue97
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue98
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js20
-rw-r--r--app/assets/javascripts/notes/stores/getters.js44
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js44
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/index.js10
-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/projects/wikis/wikis.js4
-rw-r--r--app/assets/javascripts/pages/root/index.js5
-rw-r--r--app/assets/javascripts/persistent_user_callout.js34
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue10
-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.js4
-rw-r--r--app/assets/javascripts/terminal/index.js2
-rw-r--r--app/assets/javascripts/terminal/terminal.js57
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue110
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue48
-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/notes/placeholder_note.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue50
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue1
-rw-r--r--app/assets/stylesheets/bootstrap.scss2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss9
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/callout.scss6
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss12
-rw-r--r--app/assets/stylesheets/framework/filters.scss16
-rw-r--r--app/assets/stylesheets/framework/flash.scss10
-rw-r--r--app/assets/stylesheets/framework/flex_grid.scss52
-rw-r--r--app/assets/stylesheets/framework/header.scss10
-rw-r--r--app/assets/stylesheets/framework/highlight.scss3
-rw-r--r--app/assets/stylesheets/framework/icons.scss12
-rw-r--r--app/assets/stylesheets/framework/mobile.scss9
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/timeline.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss2
-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/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss38
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss192
-rw-r--r--app/assets/stylesheets/pages/tree.scss12
-rw-r--r--app/controllers/admin/impersonations_controller.rb13
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb5
-rw-r--r--app/controllers/application_controller.rb56
-rw-r--r--app/controllers/boards/issues_controller.rb16
-rw-r--r--app/controllers/chaos_controller.rb10
-rw-r--r--app/controllers/concerns/issuable_collections.rb36
-rw-r--r--app/controllers/concerns/notes_actions.rb10
-rw-r--r--app/controllers/concerns/sessionless_authentication.rb28
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/uploads_actions.rb14
-rw-r--r--app/controllers/dashboard/projects_controller.rb1
-rw-r--r--app/controllers/dashboard/todos_controller.rb10
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/graphql_controller.rb1
-rw-r--r--app/controllers/groups/clusters_controller.rb8
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb10
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb8
-rw-r--r--app/controllers/profiles/keys_controller.rb4
-rw-r--r--app/controllers/projects/artifacts_controller.rb9
-rw-r--r--app/controllers/projects/blob_controller.rb13
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb15
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/imports_controller.rb16
-rw-r--r--app/controllers/projects/issues_controller.rb17
-rw-r--r--app/controllers/projects/jobs_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/milestones_controller.rb21
-rw-r--r--app/controllers/projects/network_controller.rb8
-rw-r--r--app/controllers/projects/pipelines_controller.rb10
-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/projects/tags_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/users_controller.rb1
-rw-r--r--app/finders/issuable_finder.rb42
-rw-r--r--app/finders/pipelines_finder.rb2
-rw-r--r--app/finders/projects/serverless/functions_finder.rb31
-rw-r--r--app/helpers/appearances_helper.rb7
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/button_helper.rb5
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/ide_helper.rb16
-rw-r--r--app/helpers/milestones_helper.rb13
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/helpers/sentry_helper.rb11
-rw-r--r--app/helpers/sorting_helper.rb59
-rw-r--r--app/helpers/users_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb2
-rw-r--r--app/helpers/workhorse_helper.rb9
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/emails/projects.rb15
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb17
-rw-r--r--app/models/ci/pipeline.rb61
-rw-r--r--app/models/ci/pipeline_enums.rb3
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/clusters/applications/jupyter.rb6
-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.rb50
-rw-r--r--app/models/clusters/kubernetes_namespace.rb4
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_collection.rb2
-rw-r--r--app/models/concerns/awardable.rb13
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb2
-rw-r--r--app/models/concerns/deployment_platform.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/shardable.rb18
-rw-r--r--app/models/concerns/token_authenticatable.rb25
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb37
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb103
-rw-r--r--app/models/concerns/with_uploads.rb31
-rw-r--r--app/models/environment_status.rb31
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/hooks/web_hook.rb4
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/member.rb19
-rw-r--r--app/models/merge_request.rb75
-rw-r--r--app/models/namespace.rb6
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/pool_repository.rb20
-rw-r--r--app/models/project.rb256
-rw-r--r--app/models/project_import_state.rb29
-rw-r--r--app/models/project_repository.rb13
-rw-r--r--app/models/project_services/kubernetes_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb4
-rw-r--r--app/models/project_services/prometheus_service.rb2
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb9
-rw-r--r--app/models/shard.rb4
-rw-r--r--app/models/storage/hashed_project.rb8
-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/models/wiki_page.rb6
-rw-r--r--app/policies/commit_policy.rb2
-rw-r--r--app/policies/note_policy.rb9
-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/README.md4
-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/issue_board_entity.rb50
-rw-r--r--app/serializers/issue_serializer.rb6
-rw-r--r--app/serializers/label_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb2
-rw-r--r--app/serializers/projects/serverless/service_entity.rb33
-rw-r--r--app/serializers/projects/serverless/service_serializer.rb9
-rw-r--r--app/services/access_token_validation_service.rb6
-rw-r--r--app/services/ci/archive_trace_service.rb35
-rw-r--r--app/services/ci/create_pipeline_service.rb15
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb14
-rw-r--r--app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb2
-rw-r--r--app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb (renamed from app/services/clusters/gcp/kubernetes/create_service_account_service.rb)11
-rw-r--r--app/services/clusters/refresh_service.rb40
-rw-r--r--app/services/files/multi_service.rb11
-rw-r--r--app/services/merge_requests/base_service.rb18
-rw-r--r--app/services/merge_requests/create_service.rb7
-rw-r--r--app/services/merge_requests/refresh_service.rb23
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/services/projects/auto_devops/disable_service.rb2
-rw-r--r--app/services/projects/cleanup_service.rb52
-rw-r--r--app/services/projects/create_service.rb14
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb1
-rw-r--r--app/services/projects/transfer_service.rb5
-rw-r--r--app/services/test_hooks/project_service.rb2
-rw-r--r--app/validators/duration_validator.rb6
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml24
-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/devise/mailer/email_changed.html.haml12
-rw-r--r--app/views/devise/mailer/email_changed.text.erb10
-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/_filter.html.haml6
-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/edit.html.haml1
-rw-r--r--app/views/groups/labels/edit.html.haml4
-rw-r--r--app/views/groups/labels/new.html.haml5
-rw-r--r--app/views/groups/milestones/edit.html.haml5
-rw-r--r--app/views/groups/milestones/new.html.haml15
-rw-r--r--app/views/ide/_show.html.haml10
-rw-r--r--app/views/ide/index.html.haml18
-rw-r--r--app/views/import/bitbucket/status.html.haml5
-rw-r--r--app/views/import/bitbucket_server/status.html.haml5
-rw-r--r--app/views/import/fogbugz/status.html.haml5
-rw-r--r--app/views/import/gitlab/status.html.haml5
-rw-r--r--app/views/import/google_code/status.html.haml5
-rw-r--r--app/views/invites/show.html.haml17
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-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/note_project_snippet_email.html.haml (renamed from app/views/notify/note_snippet_email.html.haml)0
-rw-r--r--app/views/notify/note_project_snippet_email.text.erb (renamed from app/views/notify/note_snippet_email.text.erb)0
-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/profiles/accounts/show.html.haml9
-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/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/commits/_commit.html.haml94
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml122
-rw-r--r--app/views/projects/find_file/show.html.haml2
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/labels/new.html.haml3
-rw-r--r--app/views/projects/milestones/edit.html.haml3
-rw-r--r--app/views/projects/milestones/new.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/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml32
-rw-r--r--app/views/projects/project_templates/_built_in_templates.html.haml6
-rw-r--r--app/views/projects/serverless/functions/index.html.haml15
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-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/tags/index.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml4
-rw-r--r--app/views/projects/wikis/_sidebar_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml10
-rw-r--r--app/views/projects/wikis/history.html.haml4
-rw-r--r--app/views/projects/wikis/show.html.haml6
-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/_auto_devops_implicitly_enabled_banner.html.haml2
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-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/groups/_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml10
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml20
-rw-r--r--app/views/shared/labels/_sort_dropdown.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/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/notifications/_button.html.haml6
-rw-r--r--app/views/shared/runners/_form.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml26
-rw-r--r--app/views/snippets/_snippets.html.haml2
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml8
-rw-r--r--app/views/snippets/edit.html.haml5
-rw-r--r--app/views/snippets/index.html.haml6
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/snippets/notes/_actions.html.haml4
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb14
-rw-r--r--app/workers/cluster_platform_configure_worker.rb12
-rw-r--r--app/workers/cluster_project_configure_worker.rb12
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb2
-rw-r--r--app/workers/concerns/project_import_options.rb2
-rw-r--r--app/workers/concerns/project_start_import.rb6
-rw-r--r--app/workers/delete_stored_files_worker.rb22
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb11
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb14
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb9
-rw-r--r--app/workers/pipeline_schedule_worker.rb32
-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_fork_worker.rb4
-rw-r--r--app/workers/repository_import_worker.rb4
-rw-r--r--app/workers/stuck_import_jobs_worker.rb2
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb6
-rw-r--r--changelogs/unreleased/19376-post-bfg-cleanup.yml5
-rw-r--r--changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml5
-rw-r--r--changelogs/unreleased/22548-reopen-error-message.yml6
-rw-r--r--changelogs/unreleased/33705-merge-request-rebase-api.yml5
-rw-r--r--changelogs/unreleased/34758-deployment-cluster.yml5
-rw-r--r--changelogs/unreleased/38495-calendar-activities-in-timezone.yml5
-rw-r--r--changelogs/unreleased/39849_controller_sorts.yml5
-rw-r--r--changelogs/unreleased/40385-prohibit_impersonation.yml5
-rw-r--r--changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml5
-rw-r--r--changelogs/unreleased/48889-populate-merge_commit_sha.yml6
-rw-r--r--changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml5
-rw-r--r--changelogs/unreleased/50341-cleanup-useless-project-import-attributes.yml6
-rw-r--r--changelogs/unreleased/50626-searching-users-by-the-admin-panel-wipes-query-when-using-sort.yml5
-rw-r--r--changelogs/unreleased/50839-webide-mr-dropdown-filter.yml5
-rw-r--r--changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml5
-rw-r--r--changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml5
-rw-r--r--changelogs/unreleased/51083-fix-move-api.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/51138-54026-breadcrumb-subgroups-ellipsis.yml5
-rw-r--r--changelogs/unreleased/51243-further-improvements-to-project-overview-ui.yml5
-rw-r--r--changelogs/unreleased/52285-omniauth-jwt-ppk-support.yml5
-rw-r--r--changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml5
-rw-r--r--changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml5
-rw-r--r--changelogs/unreleased/53659-use-padded-key-for-gcm-ciphers.yml5
-rw-r--r--changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml5
-rw-r--r--changelogs/unreleased/53778-remove-site-statistics.yml5
-rw-r--r--changelogs/unreleased/53994-add-missing-ci_builds-partial-indices.yml5
-rw-r--r--changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml5
-rw-r--r--changelogs/unreleased/54218-fix-mergeUrlParams.yml5
-rw-r--r--changelogs/unreleased/54282-tooltip-stuck.yml5
-rw-r--r--changelogs/unreleased/54336-include-tags-into-pipeline-detail-view.yml5
-rw-r--r--changelogs/unreleased/54571-runner-tags.yml5
-rw-r--r--changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml5
-rw-r--r--changelogs/unreleased/54826-use-read_repository-scope-on-read-only-files-endpoints.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/_acet-fix-flash-styling.yml5
-rw-r--r--changelogs/unreleased/ab-approximate-counts.yml5
-rw-r--r--changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml5
-rw-r--r--changelogs/unreleased/bump_gpgme_gem.yml5
-rw-r--r--changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml5
-rw-r--r--changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml5
-rw-r--r--changelogs/unreleased/check-if-fetched-data-does-is-complete.yml5
-rw-r--r--changelogs/unreleased/define-default-value-for-only-except-keys.yml5
-rw-r--r--changelogs/unreleased/deprecated-instance-find.yml5
-rw-r--r--changelogs/unreleased/diff-expand-commit-file.yml5
-rw-r--r--changelogs/unreleased/diff-fix-expanding.yml5
-rw-r--r--changelogs/unreleased/dm-batch-loader-sidekiq.yml5
-rw-r--r--changelogs/unreleased/dm-remove-prune-web-hook-logs-worker.yml5
-rw-r--r--changelogs/unreleased/document-raw-snippet-api.yml5
-rw-r--r--changelogs/unreleased/expose-mr-pipeline-variables.yml5
-rw-r--r--changelogs/unreleased/fix-gb-encrypt-ci-build-token.yml5
-rw-r--r--changelogs/unreleased/fix-gb-encrypt-runners-tokens.yml5
-rw-r--r--changelogs/unreleased/fix-gb-improve-timeout-inputs-help-sections.yml5
-rw-r--r--changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml5
-rw-r--r--changelogs/unreleased/fix-multiple-comments-shade-overlap.yml5
-rw-r--r--changelogs/unreleased/fix-not-render-emoji.yml5
-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-change-container-width-for-project-import.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-invites.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-snippets.yml5
-rw-r--r--changelogs/unreleased/gt-show-primary-button-when-all-labels-are-prioritized.yml5
-rw-r--r--changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml5
-rw-r--r--changelogs/unreleased/improve_auto_devops_migration_debug.yml6
-rw-r--r--changelogs/unreleased/include-new-link-in-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml5
-rw-r--r--changelogs/unreleased/jupyter-tls.yml5
-rw-r--r--changelogs/unreleased/legacy_fallback_for_project_clusters_only.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/mr-pipelines-2.yml5
-rw-r--r--changelogs/unreleased/multiple-diff-line-discussions-fix.yml5
-rw-r--r--changelogs/unreleased/non-webkit-scrollbar-fixing.yml5
-rw-r--r--changelogs/unreleased/order-of-notification-settings.yml5
-rw-r--r--changelogs/unreleased/osw-fix-grouping-by-file-path.yml5
-rw-r--r--changelogs/unreleased/osw-remove-unnused-data-from-diff-discussions.yml5
-rw-r--r--changelogs/unreleased/remove-blob-search-limit.yml5
-rw-r--r--changelogs/unreleased/remove-deployment-status-hack-from-backend.yml5
-rw-r--r--changelogs/unreleased/render-text-deprecated.yml6
-rw-r--r--changelogs/unreleased/retryable_create_or_update_kubernetes_namespace.yml6
-rw-r--r--changelogs/unreleased/security-182-update-workhorse.yml5
-rw-r--r--changelogs/unreleased/security-2736-prometheus-ssrf.yml5
-rw-r--r--changelogs/unreleased/security-bvl-exposure-in-commits-list.yml5
-rw-r--r--changelogs/unreleased/security-email-change-notification.yml5
-rw-r--r--changelogs/unreleased/security-fix-pat-web-access.yml5
-rw-r--r--changelogs/unreleased/security-fix-uri-xss-applications.yml5
-rw-r--r--changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml5
-rw-r--r--changelogs/unreleased/security-fj-crlf-injection.yml5
-rw-r--r--changelogs/unreleased/security-guest-comments.yml5
-rw-r--r--changelogs/unreleased/security-guest-comments_2.yml5
-rw-r--r--changelogs/unreleased/security-issue_51301.yml5
-rw-r--r--changelogs/unreleased/security-mermaid-xss.yml5
-rw-r--r--changelogs/unreleased/security-pages-toctou-race.yml6
-rw-r--r--changelogs/unreleased/security-private-group.yml6
-rw-r--r--changelogs/unreleased/security-stored-xss-for-environments.yml5
-rw-r--r--changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml5
-rw-r--r--changelogs/unreleased/set-kubeconfig-nil-when-token-nil.yml5
-rw-r--r--changelogs/unreleased/sh-disable-autocomplete-mirror-settings.yml5
-rw-r--r--changelogs/unreleased/sh-fix-hash-filename-handling.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-38317.yml5
-rw-r--r--changelogs/unreleased/sh-fix-mirrors-protected-branches.yml5
-rw-r--r--changelogs/unreleased/sh-handle-invalid-gpg-sig.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-full-path-config.yml5
-rw-r--r--changelogs/unreleased/tc-repo-full-path-in-db.yml5
-rw-r--r--changelogs/unreleased/triggermesh-phase2-serverless-list.yml5
-rw-r--r--changelogs/unreleased/unicorn-monkey-patch.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-39.yml5
-rw-r--r--changelogs/unreleased/usage-count.yml5
-rw-r--r--changelogs/unreleased/winh-collapse-discussions.yml5
-rw-r--r--changelogs/unreleased/winh-divider-margin.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-divider-color.yml5
-rw-r--r--changelogs/unreleased/winh-issue-boards-project-dropdown-close.yml5
-rw-r--r--changelogs/unreleased/winh-merge-request-commit-discussion.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/workhorse-7-3-0.yml5
-rw-r--r--config.ru4
-rw-r--r--config/application.rb3
-rw-r--r--config/dependency_decisions.yml10
-rw-r--r--config/gitlab.yml.example19
-rw-r--r--config/initializers/1_settings.rb5
-rw-r--r--config/initializers/action_dispatch_http_mime_negotiation.rb19
-rw-r--r--config/initializers/correlation_id.rb3
-rw-r--r--config/initializers/devise.rb3
-rw-r--r--config/initializers/doorkeeper.rb7
-rw-r--r--config/initializers/lograge.rb1
-rw-r--r--config/initializers/rack_attack_global.rb10
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/initializers/sidekiq.rb5
-rw-r--r--config/jest.config.js27
-rw-r--r--config/routes/project.rb5
-rw-r--r--config/routes/wiki.rb2
-rw-r--r--config/settings.rb8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--danger/changelog/Dangerfile2
-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/14_pipelines.rb2
-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/20181108091549_cleanup_environments_external_url.rb18
-rw-r--r--db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb11
-rw-r--r--db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb11
-rw-r--r--db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb11
-rw-r--r--db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb13
-rw-r--r--db/migrate/20181120082911_rename_repositories_pool_repositories.rb11
-rw-r--r--db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb22
-rw-r--r--db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb11
-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/20181122160027_create_project_repositories.rb18
-rw-r--r--db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb9
-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/20181010133639_backfill_store_project_full_path_in_repo.rb27
-rw-r--r--db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb32
-rw-r--r--db/post_migrate/20181121111200_schedule_runners_token_encryption.rb38
-rw-r--r--db/post_migrate/20181123042307_drop_site_statistics.rb (renamed from db/migrate/20181123042307_drop_site_statistics.rb)0
-rw-r--r--db/schema.rb42
-rw-r--r--doc/README.md102
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/jwt.md36
-rw-r--r--doc/administration/gitaly/index.md11
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md17
-rw-r--r--doc/api/README.md36
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/merge_requests.md70
-rw-r--r--doc/api/repositories.md8
-rw-r--r--doc/api/repository_files.md10
-rw-r--r--doc/api/repository_submodules.md2
-rw-r--r--doc/api/search.md28
-rw-r--r--doc/api/snippets.md40
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/docker/using_docker_images.md3
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md6
-rw-r--r--doc/ci/interactive_web_terminal/index.md9
-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/variables/README.md162
-rw-r--r--doc/ci/yaml/README.md46
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/automatic_ce_ee_merge.md283
-rw-r--r--doc/development/code_review.md71
-rw-r--r--doc/development/documentation/index.md11
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/fe_guide/graphql.md83
-rw-r--r--doc/development/fe_guide/index.md3
-rw-r--r--doc/development/feature_flags.md10
-rw-r--r--doc/development/github_importer.md2
-rw-r--r--doc/development/gotchas.md6
-rw-r--r--doc/development/i18n/proofreader.md4
-rw-r--r--doc/development/new_fe_guide/development/testing.md8
-rw-r--r--doc/development/profiling.md7
-rw-r--r--doc/development/testing_guide/ci.md4
-rw-r--r--doc/development/testing_guide/review_apps.md119
-rw-r--r--doc/install/docker.md7
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/raketasks/web_hooks.md6
-rw-r--r--doc/security/two_factor_authentication.md2
-rw-r--r--doc/topics/authentication/index.md1
-rw-r--r--doc/topics/autodevops/index.md24
-rw-r--r--doc/update/11.5-to-11.6.md390
-rw-r--r--doc/user/admin_area/custom_project_templates.md25
-rw-r--r--doc/user/group/clusters/index.md126
-rw-r--r--doc/user/group/custom_project_templates.md23
-rw-r--r--doc/user/group/index.md8
-rw-r--r--doc/user/group/subgroups/index.md1
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/project/clusters/index.md123
-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/import/bitbucket_server.md2
-rw-r--r--doc/user/project/integrations/emails_on_push.md16
-rw-r--r--doc/user/project/integrations/img/emails_on_push_email.pngbin0 -> 62816 bytes
-rw-r--r--doc/user/project/integrations/img/emails_on_push_service.pngbin28535 -> 26788 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_widget.pngbin11036 -> 8936 bytes
-rw-r--r--doc/user/project/merge_requests/index.md10
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md36
-rw-r--r--doc/user/project/pipelines/schedules.md12
-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/workflow/notifications.md2
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/api.rb8
-rw-r--r--lib/api/api_guard.rb6
-rw-r--r--lib/api/award_emoji.rb2
-rw-r--r--lib/api/badges.rb2
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb4
-rw-r--r--lib/api/commit_statuses.rb6
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/discussions.rb2
-rw-r--r--lib/api/entities.rb13
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/files.rb6
-rw-r--r--lib/api/group_boards.rb2
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/group_variables.rb2
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/issues.rb4
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb4
-rw-r--r--lib/api/labels.rb2
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb39
-rw-r--r--lib/api/namespaces.rb19
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/pages_domains.rb4
-rw-r--r--lib/api/pipeline_schedules.rb2
-rw-r--r--lib/api/pipelines.rb4
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_import.rb2
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/protected_branches.rb4
-rw-r--r--lib/api/protected_tags.rb4
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/resource_label_events.rb2
-rw-r--r--lib/api/runner.rb6
-rw-r--r--lib/api/runners.rb2
-rw-r--r--lib/api/search.rb11
-rw-r--r--lib/api/services.rb4
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/tags.rb4
-rw-r--r--lib/api/templates.rb2
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/variables.rb2
-rw-r--r--lib/api/wikis.rb4
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb3
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb5
-rw-r--r--lib/extracts_path.rb5
-rw-r--r--lib/gitlab/auth/request_authenticator.rb14
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb42
-rw-r--r--lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb209
-rw-r--r--lib/gitlab/background_migration/encrypt_columns.rb19
-rw-r--r--lib/gitlab/background_migration/encrypt_runners_tokens.rb32
-rw-r--r--lib/gitlab/background_migration/models/encrypt_columns/namespace.rb28
-rw-r--r--lib/gitlab/background_migration/models/encrypt_columns/project.rb28
-rw-r--r--lib/gitlab/background_migration/models/encrypt_columns/runner.rb28
-rw-r--r--lib/gitlab/background_migration/models/encrypt_columns/settings.rb37
-rw-r--r--lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb4
-rw-r--r--lib/gitlab/badge/coverage/report.rb2
-rw-r--r--lib/gitlab/badge/pipeline/status.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb2
-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/base_checker.rb38
-rw-r--r--lib/gitlab/checks/branch_check.rb110
-rw-r--r--lib/gitlab/checks/change_access.rb234
-rw-r--r--lib/gitlab/checks/commit_check.rb65
-rw-r--r--lib/gitlab/checks/diff_check.rb99
-rw-r--r--lib/gitlab/checks/lfs_check.rb22
-rw-r--r--lib/gitlab/checks/push_check.rb22
-rw-r--r--lib/gitlab/checks/tag_check.rb46
-rw-r--r--lib/gitlab/ci/charts.rb4
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb8
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb29
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb20
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb4
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb83
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb4
-rw-r--r--lib/gitlab/ci/config/entry/except_policy.rb17
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb75
-rw-r--r--lib/gitlab/ci/config/entry/global.rb6
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb4
-rw-r--r--lib/gitlab/ci/config/entry/image.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb10
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb6
-rw-r--r--lib/gitlab/ci/config/entry/key.rb4
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb72
-rw-r--r--lib/gitlab/ci/config/entry/node.rb103
-rw-r--r--lib/gitlab/ci/config/entry/only_policy.rb18
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb4
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb29
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb6
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb14
-rw-r--r--lib/gitlab/ci/config/entry/script.rb4
-rw-r--r--lib/gitlab/ci/config/entry/service.rb2
-rw-r--r--lib/gitlab/ci/config/entry/services.rb6
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb45
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb4
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb42
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb21
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb40
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb28
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb198
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/base.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml35
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/config/entry/attributable.rb27
-rw-r--r--lib/gitlab/config/entry/boolean.rb18
-rw-r--r--lib/gitlab/config/entry/configurable.rb81
-rw-r--r--lib/gitlab/config/entry/factory.rb73
-rw-r--r--lib/gitlab/config/entry/legacy_validation_helpers.rb70
-rw-r--r--lib/gitlab/config/entry/node.rb101
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb43
-rw-r--r--lib/gitlab/config/entry/undefined.rb40
-rw-r--r--lib/gitlab/config/entry/unspecified.rb19
-rw-r--r--lib/gitlab/config/entry/validatable.rb38
-rw-r--r--lib/gitlab/config/entry/validator.rb26
-rw-r--r--lib/gitlab/config/entry/validators.rb196
-rw-r--r--lib/gitlab/config/loader/format_error.rb9
-rw-r--r--lib/gitlab/config/loader/yaml.rb (renamed from lib/gitlab/ci/config/loader.rb)12
-rw-r--r--lib/gitlab/correlation_id.rb40
-rw-r--r--lib/gitlab/crypto_helper.rb6
-rw-r--r--lib/gitlab/database/count.rb79
-rw-r--r--lib/gitlab/database/count/exact_count_strategy.rb33
-rw-r--r--lib/gitlab/database/count/reltuples_count_strategy.rb79
-rw-r--r--lib/gitlab/database/count/tablesample_count_strategy.rb66
-rw-r--r--lib/gitlab/database/migration_helpers.rb3
-rw-r--r--lib/gitlab/diff/file_collection/base.rb10
-rw-r--r--lib/gitlab/diff/file_collection/compare.rb4
-rw-r--r--lib/gitlab/file_finder.rb57
-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/operation_service.rb3
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb2
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb3
-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/group_hierarchy.rb29
-rw-r--r--lib/gitlab/import_export/import_export.yml16
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb4
-rw-r--r--lib/gitlab/import_export/relation_rename_service.rb48
-rw-r--r--lib/gitlab/json_logger.rb1
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb57
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb3
-rw-r--r--lib/gitlab/lfs_token.rb2
-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/template/finders/global_template_finder.rb4
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb5
-rw-r--r--lib/gitlab/url_blocker.rb37
-rw-r--r--lib/gitlab/usage_data.rb22
-rw-r--r--lib/gitlab/utils.rb24
-rw-r--r--lib/gitlab/wiki_file_finder.rb6
-rw-r--r--lib/gitlab/workhorse.rb1
-rw-r--r--lib/omni_auth/strategies/jwt.rb17
-rw-r--r--lib/system_check/gitaly_check.rb19
-rw-r--r--lib/system_check/gitlab_shell_check.rb56
-rw-r--r--lib/system_check/incoming_email_check.rb27
-rw-r--r--lib/system_check/ldap_check.rb60
-rw-r--r--lib/system_check/orphans/repository_check.rb1
-rw-r--r--lib/system_check/rake_task/app_task.rb38
-rw-r--r--lib/system_check/rake_task/gitaly_task.rb18
-rw-r--r--lib/system_check/rake_task/gitlab_shell_task.rb18
-rw-r--r--lib/system_check/rake_task/gitlab_task.rb33
-rw-r--r--lib/system_check/rake_task/incoming_email_task.rb18
-rw-r--r--lib/system_check/rake_task/ldap_task.rb18
-rw-r--r--lib/system_check/rake_task/orphans/namespace_task.rb20
-rw-r--r--lib/system_check/rake_task/orphans/repository_task.rb20
-rw-r--r--lib/system_check/rake_task/orphans_task.rb21
-rw-r--r--lib/system_check/rake_task/rake_task_helpers.rb32
-rw-r--r--lib/system_check/rake_task/sidekiq_task.rb18
-rw-r--r--lib/system_check/sidekiq_check.rb58
-rw-r--r--lib/tasks/gettext.rake4
-rw-r--r--lib/tasks/gitlab/check.rake259
-rw-r--r--lib/tasks/gitlab/cleanup.rake4
-rw-r--r--lib/tasks/gitlab/web_hook.rake45
-rw-r--r--lib/tasks/import.rake4
-rw-r--r--locale/gitlab.pot274
-rw-r--r--package.json21
-rw-r--r--public/robots.txt2
-rw-r--r--qa/README.md9
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/page/base.rb20
-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/runtime/browser.rb9
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb11
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb45
-rw-r--r--qa/qa/support/page/logging.rb20
-rw-r--r--rubocop/cop/migration/add_reference.rb9
-rwxr-xr-xscripts/review_apps/review-apps.sh146
-rwxr-xr-xscripts/trigger-build14
-rw-r--r--spec/config/settings_spec.rb98
-rw-r--r--spec/controllers/admin/users_controller_spec.rb12
-rw-r--r--spec/controllers/application_controller_spec.rb165
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb6
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb1
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb5
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb10
-rw-r--r--spec/controllers/dashboard_controller_spec.rb31
-rw-r--r--spec/controllers/graphql_controller_spec.rb47
-rw-r--r--spec/controllers/groups_controller_spec.rb23
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb17
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb35
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb5
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb138
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb9
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb9
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb36
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb58
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb12
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb14
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb33
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb103
-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/tags_controller_spec.rb22
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb82
-rw-r--r--spec/controllers/projects_controller_spec.rb22
-rw-r--r--spec/controllers/snippets_controller_spec.rb21
-rw-r--r--spec/controllers/users_controller_spec.rb8
-rw-r--r--spec/factories/ci/builds.rb2
-rw-r--r--spec/factories/clusters/kubernetes_namespaces.rb8
-rw-r--r--spec/factories/import_state.rb7
-rw-r--r--spec/factories/pool_repositories.rb5
-rw-r--r--spec/factories/projects.rb9
-rw-r--r--spec/factories/shards.rb5
-rw-r--r--spec/features/admin/admin_users_spec.rb154
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb1
-rw-r--r--spec/features/groups/members/list_members_spec.rb9
-rw-r--r--spec/features/groups/members/manage_members_spec.rb9
-rw-r--r--spec/features/ide_spec.rb2
-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/dropdown_label_spec.rb13
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb4
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb12
-rw-r--r--spec/features/issues/user_sees_breadcrumb_links_spec.rb8
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb8
-rw-r--r--spec/features/markdown/mermaid_spec.rb2
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb26
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_breadcrumb_links_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb31
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb21
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb365
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb12
-rw-r--r--spec/features/milestones/user_promotes_milestone_spec.rb32
-rw-r--r--spec/features/milestones/user_sees_breadcrumb_links_spec.rb19
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb23
-rw-r--r--spec/features/projects/environments/environment_spec.rb10
-rw-r--r--spec/features/projects/jobs_spec.rb10
-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/labels/user_sees_breadcrumb_links_spec.rb17
-rw-r--r--spec/features/projects/members/list_spec.rb9
-rw-r--r--spec/features/projects/pages_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb150
-rw-r--r--spec/features/projects/serverless/functions_spec.rb49
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb89
-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/projects/wiki/user_creates_wiki_page_spec.rb16
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb6
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb8
-rw-r--r--spec/features/projects_spec.rb2
-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/entities/issue_board.json2
-rw-r--r--spec/fixtures/api/schemas/entities/issue_boards.json15
-rw-r--r--spec/fixtures/bfg_object_map.txt1
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json190
-rw-r--r--spec/fixtures/security-reports/master/gl-dependency-scanning-report.json185
-rw-r--r--spec/frontend/.eslintrc.yml9
-rw-r--r--spec/frontend/dummy_spec.js1
-rw-r--r--spec/helpers/projects_helper_spec.rb25
-rw-r--r--spec/helpers/sorting_helper_spec.rb43
-rw-r--r--spec/initializers/lograge_spec.rb38
-rw-r--r--spec/javascripts/api_spec.js29
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js63
-rw-r--r--spec/javascripts/diffs/components/app_spec.js13
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js6
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js26
-rw-r--r--spec/javascripts/diffs/components/diff_gutter_avatars_spec.js29
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js1
-rw-r--r--spec/javascripts/diffs/components/diff_table_cell_spec.js37
-rw-r--r--spec/javascripts/diffs/components/inline_diff_table_row_spec.js42
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_table_row_spec.js85
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js2
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js106
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js71
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js149
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js26
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js56
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js12
-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/url_utility_spec.js24
-rw-r--r--spec/javascripts/monitoring/graph_spec.js20
-rw-r--r--spec/javascripts/monitoring/mock_data.js53
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js34
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js3
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js83
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js60
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js26
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js57
-rw-r--r--spec/javascripts/pipelines/graph/job_item_spec.js52
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js30
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js90
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js13
-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/notes/timeline_entry_item_spec.js40
-rw-r--r--spec/lib/banzai/pipeline/gfm_pipeline_spec.rb12
-rw-r--r--spec/lib/gitlab/auth/request_authenticator_spec.rb18
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb94
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb85
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_runners_tokens_spec.rb77
-rw-r--r--spec/lib/gitlab/branch_push_merge_commit_analyzer_spec.rb62
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb90
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb241
-rw-r--r--spec/lib/gitlab/checks/diff_check_spec.rb51
-rw-r--r--spec/lib/gitlab/checks/lfs_check_spec.rb52
-rw-r--r--spec/lib/gitlab/checks/push_check_spec.rb22
-rw-r--r--spec/lib/gitlab/checks/tag_check_spec.rb64
-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/pipeline/chain/build_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb30
-rw-r--r--spec/lib/gitlab/config/entry/attributable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/attributable_spec.rb)6
-rw-r--r--spec/lib/gitlab/config/entry/boolean_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/boolean_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/configurable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/configurable_spec.rb)8
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/factory_spec.rb)14
-rw-r--r--spec/lib/gitlab/config/entry/simplifiable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/undefined_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/undefined_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/unspecified_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/unspecified_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/validatable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/validatable_spec.rb)8
-rw-r--r--spec/lib/gitlab/config/entry/validator_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/validator_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb (renamed from spec/lib/gitlab/ci/config/loader_spec.rb)6
-rw-r--r--spec/lib/gitlab/correlation_id_spec.rb77
-rw-r--r--spec/lib/gitlab/crypto_helper_spec.rb37
-rw-r--r--spec/lib/gitlab/database/count/exact_count_strategy_spec.rb40
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb48
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb65
-rw-r--r--spec/lib/gitlab/database/count_spec.rb72
-rw-r--r--spec/lib/gitlab/diff/file_collection/commit_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb27
-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/github_import/importer/repository_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb22
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml12
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb111
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb6
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb79
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb24
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb55
-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.rb120
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb41
-rw-r--r--spec/lib/gitlab/utils_spec.rb66
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb70
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/migrations/backfill_store_project_full_path_in_repo_spec.rb98
-rw-r--r--spec/migrations/clean_up_for_members_spec.rb5
-rw-r--r--spec/migrations/cleanup_environments_external_url_spec.rb28
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb15
-rw-r--r--spec/migrations/migrate_forbidden_redirect_uris_spec.rb48
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb12
-rw-r--r--spec/migrations/schedule_runners_token_encryption_spec.rb38
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/blob_spec.rb3
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb30
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb124
-rw-r--r--spec/models/ci/job_artifact_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb329
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/ci/stage_spec.rb2
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb2
-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/clusters/cluster_spec.rb122
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb32
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/commit_status_spec.rb2
-rw-r--r--spec/models/concerns/chronic_duration_attribute_spec.rb3
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb75
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb28
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb121
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/base_spec.rb65
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb156
-rw-r--r--spec/models/deployment_spec.rb2
-rw-r--r--spec/models/environment_status_spec.rb39
-rw-r--r--spec/models/gpg_signature_spec.rb2
-rw-r--r--spec/models/group_spec.rb35
-rw-r--r--spec/models/internal_id_spec.rb2
-rw-r--r--spec/models/list_spec.rb2
-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.rb15
-rw-r--r--spec/models/merge_request_spec.rb113
-rw-r--r--spec/models/namespace_spec.rb6
-rw-r--r--spec/models/note_spec.rb2
-rw-r--r--spec/models/notification_setting_spec.rb2
-rw-r--r--spec/models/pool_repository_spec.rb26
-rw-r--r--spec/models/project_auto_devops_spec.rb2
-rw-r--r--spec/models/project_import_state_spec.rb112
-rw-r--r--spec/models/project_repository_spec.rb23
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb17
-rw-r--r--spec/models/project_spec.rb445
-rw-r--r--spec/models/prometheus_metric_spec.rb2
-rw-r--r--spec/models/push_event_payload_spec.rb2
-rw-r--r--spec/models/remote_mirror_spec.rb8
-rw-r--r--spec/models/repository_spec.rb40
-rw-r--r--spec/models/resource_label_event_spec.rb2
-rw-r--r--spec/models/todo_spec.rb3
-rw-r--r--spec/models/uploads/fog_spec.rb69
-rw-r--r--spec/models/uploads/local_spec.rb45
-rw-r--r--spec/models/user_callout_spec.rb2
-rw-r--r--spec/models/user_spec.rb14
-rw-r--r--spec/policies/note_policy_spec.rb79
-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/applications_spec.rb12
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb47
-rw-r--r--spec/requests/api/helpers_spec.rb43
-rw-r--r--spec/requests/api/members_spec.rb31
-rw-r--r--spec/requests/api/merge_requests_spec.rb30
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb12
-rw-r--r--spec/requests/api/project_import_spec.rb10
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb2
-rw-r--r--spec/routing/project_routing_spec.rb20
-rw-r--r--spec/rubocop/cop/migration/add_reference_spec.rb12
-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/issue_board_entity_spec.rb23
-rw-r--r--spec/serializers/issue_serializer_spec.rb8
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb51
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb258
-rw-r--r--spec/services/ci/retry_build_service_spec.rb6
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb31
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb133
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb (renamed from spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb)12
-rw-r--r--spec/services/clusters/refresh_service_spec.rb107
-rw-r--r--spec/services/files/multi_service_spec.rb39
-rw-r--r--spec/services/merge_requests/create_service_spec.rb72
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb161
-rw-r--r--spec/services/notification_service_spec.rb21
-rw-r--r--spec/services/projects/cleanup_service_spec.rb44
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb26
-rw-r--r--spec/services/projects/transfer_service_spec.rb26
-rw-r--r--spec/support/active_record_enum.rb12
-rw-r--r--spec/support/controllers/sessionless_auth_controller_shared_examples.rb92
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb31
-rw-r--r--spec/support/helpers/features/list_rows_helpers.rb28
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb4
-rw-r--r--spec/support/helpers/git_http_helpers.rb5
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb86
-rw-r--r--spec/support/helpers/prometheus_helpers.rb4
-rw-r--r--spec/support/helpers/sorting_helper.rb2
-rw-r--r--spec/support/helpers/stub_configuration.rb5
-rw-r--r--spec/support/helpers/test_env.rb3
-rw-r--r--spec/support/shared_contexts/change_access_checks_shared_context.rb29
-rw-r--r--spec/support/shared_contexts/url_shared_context.rb17
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/diff_file_collections.rb16
-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/only_except_policy_examples.rb167
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_examples.rb46
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb108
-rw-r--r--spec/tasks/gitlab/web_hook_rake_spec.rb92
-rw-r--r--spec/validators/url_validator_spec.rb26
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb (renamed from spec/features/admin/admin_active_tab_spec.rb)34
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/workers/archive_trace_worker_spec.rb8
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb10
-rw-r--r--spec/workers/cluster_platform_configure_worker_spec.rb52
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb6
-rw-r--r--spec/workers/concerns/project_import_options_spec.rb14
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb30
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb22
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb3
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb5
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb87
-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_import_worker_spec.rb29
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb28
-rw-r--r--vendor/jupyter/values.yaml1
-rw-r--r--yarn.lock1834
1208 files changed, 24196 insertions, 7630 deletions
diff --git a/.babelrc.js b/.babelrc.js
index 27caf378b99..bfcc7d96634 100644
--- a/.babelrc.js
+++ b/.babelrc.js
@@ -35,4 +35,10 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
+// Jest is running in node environment
+if (BABEL_ENV === 'jest') {
+ plugins.push('transform-es2015-modules-commonjs');
+ plugins.push('dynamic-import-node');
+}
+
module.exports = { presets, plugins };
diff --git a/.eslintignore b/.eslintignore
index 33a8186fade..f78840e67be 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,6 +2,7 @@
/config/
/builds/
/coverage/
+/coverage-frontend/
/coverage-javascript/
/node_modules/
/public/
diff --git a/.gitignore b/.gitignore
index aecaae95b8c..65f61e1fade 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,5 +78,5 @@ eslint-report.html
/plugins/*
/.gitlab_pages_secret
package-lock.json
-/junit_rspec.xml
-/junit_karma.xml
+/junit_*.xml
+/coverage-frontend/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2c521b03863..46604317232 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -48,6 +48,7 @@ after_script:
stages:
- build
- prepare
+ - merge
- test
- post-test
- pages
@@ -439,6 +440,50 @@ setup-test-env:
- config/secrets.yml
- vendor/gitaly-ruby
+# GitLab Review apps
+.review-base: &review-base
+ <<: *dedicated-no-docs-no-db-pull-cache-job
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: test
+ cache: {}
+ dependencies: []
+ environment: &review-environment
+ name: review/${CI_COMMIT_REF_NAME}
+ url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ refs:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+ before_script: []
+
+.review-docker: &review-docker
+ <<: *review-base
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
+ services:
+ - docker:stable-dind
+ tags:
+ - gitlab-org
+ - docker
+ variables: &review-docker-variables
+ GIT_DEPTH: "1"
+ DOCKER_DRIVER: overlay2
+ DOCKER_HOST: tcp://docker:2375
+ LATEST_QA_IMAGE: "gitlab/${CI_PROJECT_NAME}-qa:nightly"
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab/${CI_PROJECT_NAME}-qa:${CI_COMMIT_REF_SLUG}"
+
+build-qa-image:
+ <<: *review-docker
+ stage: prepare
+ script:
+ - time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/
+ - echo "${CI_JOB_TOKEN}" | docker login --username gitlab-ci-token --password-stdin ${CI_REGISTRY}
+ - time docker push ${QA_IMAGE}
+
danger-review:
<<: *pull-cache
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
@@ -622,7 +667,7 @@ gitlab:assets:compile:
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
- - //@gitlab/gitabhq
+ - //@gitlab/gitlabhq
- //@gitlab/gitlab-ee
tags:
- gitlab-org-delivery
@@ -654,6 +699,32 @@ karma:
reports:
junit: junit_karma.xml
+jest:
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
+ <<: *use-pg
+ dependencies:
+ - compile-assets
+ - setup-test-env
+ script:
+ - scripts/gitaly-test-spawn
+ - date
+ - bundle exec rake karma:fixtures
+ - date
+ - yarn jest --ci --coverage
+ artifacts:
+ name: coverage-frontend
+ expire_in: 31d
+ when: always
+ paths:
+ - coverage-frontend/
+ - junit_jest.xml
+ reports:
+ junit: junit_jest.xml
+ cache:
+ key: jest
+ paths:
+ - tmp/jest/jest/
+
code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
@@ -876,85 +947,102 @@ no_ee_check:
- //@gitlab-org/gitlab-ce
# GitLab Review apps
-review:
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- stage: test
+review-deploy:
+ <<: *review-base
+ retry: 2
allow_failure: true
- before_script:
- - gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
- HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG"
- DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN"
+ HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
+ DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
+ API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
+ environment:
+ <<: *review-environment
+ on_stop: review-stop
+ before_script:
+ - apk update && apk add jq
+ - gem install gitlab --no-document
script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- source ./scripts/review_apps/review-apps.sh
+ - wait_for_job_to_be_done "gitlab:assets:compile"
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
- check_kube_domain
- download_gitlab_chart
- ensure_namespace
- install_tiller
- install_external_dns
- - deploy
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN
- on_stop: stop_review
- only:
- refs:
- - branches@gitlab-org/gitlab-ce
- - branches@gitlab-org/gitlab-ee
- kubernetes: active
- except:
- refs:
- - master
- - /(^docs[\/-].*|.*-docs$)/
+ - time deploy
+ - add_license
+
+.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}"
+ QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
+ QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
+ GITLAB_USERNAME: "root"
+ GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
+ GITLAB_ADMIN_USERNAME: "root"
+ GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
+ GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
+ EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
+ QA_DEBUG: "true"
+ artifacts:
+ paths:
+ - ./qa/gitlab-qa-run-*
+ expire_in: 7 days
+ when: always
+ before_script:
+ - echo "${QA_IMAGE}"
+ - echo "${CI_ENVIRONMENT_URL}"
+ - apk update && apk add curl jq
+ - source ./scripts/review_apps/review-apps.sh
+ - gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
+ - wait_for_job_to_be_done "review-deploy"
-stop_review:
+review-qa-smoke:
+ <<: *review-qa-base
+ script:
+ - gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
+
+review-qa-all:
+ <<: *review-qa-base
+ script:
+ - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
+ when: manual
+
+review-stop:
+ <<: *review-base
<<: *single-script-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- stage: test
allow_failure: true
- cache: {}
- dependencies: []
variables:
+ <<: *single-script-job-variables
SCRIPT_NAME: "review_apps/review-apps.sh"
+ when: manual
+ environment:
+ <<: *review-environment
+ action: stop
script:
- source $(basename "${SCRIPT_NAME}")
- delete
- cleanup
- when: manual
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- only:
- refs:
- - branches@gitlab-org/gitlab-ce
- - branches@gitlab-org/gitlab-ee
- kubernetes: active
- except:
- - master
- - /(^docs[\/-].*|.*-docs$)/
-schedule:review_apps_cleanup:
- <<: *dedicated-no-docs-pull-cache-job
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+schedule:review-cleanup:
+ <<: *review-base
stage: build
allow_failure: true
- cache: {}
- dependencies: []
- before_script:
- - gem install gitlab --no-document
variables:
GIT_DEPTH: "1"
- script:
- - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
environment:
name: review/auto-cleanup
- action: stop
only:
refs:
- schedules@gitlab-org/gitlab-ce
@@ -963,3 +1051,7 @@ schedule:review_apps_cleanup:
except:
- tags
- /(^docs[\/-].*|.*-docs$)/
+ before_script:
+ - gem install gitlab --no-document
+ script:
+ - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS
index a4b773b15a9..a4b773b15a9 100644
--- a/.gitlab/CODEOWNERS.disabled
+++ b/.gitlab/CODEOWNERS
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 69cf7fe1548..08651195d98 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
+- [ ] Add a link to this issue on the original security issue.
#### Backports
@@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
+- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed.
### Summary
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c65b584e3c1..d1e324c5518 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,52 @@
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)
+
+- Removed Site Statistics optimization as it was causing problems. !23314
+
+### Fixed (6 changes, 1 of them is from the community)
+
+- Display impersonation token value only after creation. !22916
+- Fix not render emoji in filter dropdown. !23112 (Hiroyuki Sato)
+- Fixes stuck tooltip on stop env button. !23244
+- Correctly handle data-loss scenarios when encrypting columns. !23306
+- Clear BatchLoader context between Sidekiq jobs. !23308
+- Fix handling of filenames with hash characters in tree view. !23368
+
+
+## 11.5.1 (2018-11-26)
+
+### Security (17 changes)
+
+- Escape user fullname while rendering autocomplete template to prevent XSS.
+- Fix CRLF vulnerability in Project hooks.
+- Fix possible XSS attack in Markdown urls with spaces.
+- Redact sensitive information on gitlab-workhorse log.
+- Do not follow redirects in Prometheus service when making http requests to the configured api url.
+- Don't expose confidential information in commit message list.
+- Provide email notification when a user changes their email address.
+- Restrict Personal Access Tokens to API scope on web requests.
+- Resolve reflected XSS in Ouath authorize window.
+- Fix SSRF in project integrations.
+- Fixed ability to comment on locked/confidential issues.
+- Fixed ability of guest users to edit/delete comments on locked or confidential issues.
+- Fix milestone promotion authorization check.
+- Configure mermaid to not render HTML content in diagrams.
+- Fix a possible symlink time of check to time of use race condition in GitLab Pages.
+- Removed ability to see private group names when the group id is entered in the url.
+- Fix stored XSS for Environments.
+
+
## 11.5.0 (2018-11-22)
### Security (10 changes, 1 of them is from the community)
@@ -264,6 +310,44 @@ entry.
- Disables stop environment button while the deploy is in progress.
+## 11.4.9 (2018-12-03)
+
+### Fixed (2 changes)
+
+- Display impersonation token value only after creation. !22916
+- Correctly handle data-loss scenarios when encrypting columns. !23306
+
+
+## 11.4.8 (2018-11-27)
+
+### Security (24 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2571
+- Resolve reflected XSS in Ouath authorize window.
+- Fix XSS in merge request source branch name.
+- Escape user fullname while rendering autocomplete template to prevent XSS.
+- Fix CRLF vulnerability in Project hooks.
+- Fix possible XSS attack in Markdown urls with spaces.
+- Redact sensitive information on gitlab-workhorse log.
+- Do not follow redirects in Prometheus service when making http requests to the configured api url.
+- Persist only SHA digest of PersonalAccessToken#token.
+- Don't expose confidential information in commit message list.
+- Provide email notification when a user changes their email address.
+- Restrict Personal Access Tokens to API scope on web requests.
+- Redact personal tokens in unsubscribe links.
+- Fix SSRF in project integrations.
+- Fixed ability to comment on locked/confidential issues.
+- Fixed ability of guest users to edit/delete comments on locked or confidential issues.
+- Fix milestone promotion authorization check.
+- Monkey kubeclient to not follow any redirects.
+- Configure mermaid to not render HTML content in diagrams.
+- Fix a possible symlink time of check to time of use race condition in GitLab Pages.
+- Removed ability to see private group names when the group id is entered in the url.
+- Fix stored XSS for Environments.
+- Prevent SSRF attacks in HipChat integration.
+- Validate Wiki attachments are valid temporary files.
+
+
## 11.4.7 (2018-11-20)
- No changes.
@@ -544,6 +628,52 @@ 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)
+
+- Filter user sensitive data from discussions JSON. !2537
+- Escape entity title while autocomplete template rendering to prevent XSS. !2557
+- Restrict Personal Access Tokens to API scope on web requests.
+- Fix XSS in merge request source branch name.
+- Escape user fullname while rendering autocomplete template to prevent XSS.
+- Fix CRLF vulnerability in Project hooks.
+- Fix possible XSS attack in Markdown urls with spaces.
+- Redact sensitive information on gitlab-workhorse log.
+- Set timeout for syntax highlighting.
+- Do not follow redirects in Prometheus service when making http requests to the configured api url.
+- Persist only SHA digest of PersonalAccessToken#token.
+- Sanitize JSON data properly to fix XSS on Issue details page.
+- Don't expose confidential information in commit message list.
+- Markdown API no longer displays confidential title references unless authorized.
+- Provide email notification when a user changes their email address.
+- Properly filter private references from system notes.
+- Redact personal tokens in unsubscribe links.
+- Resolve reflected XSS in Ouath authorize window.
+- Fix SSRF in project integrations.
+- Fix stored XSS in merge requests from imported repository.
+- Fixed ability to comment on locked/confidential issues.
+- Fixed ability of guest users to edit/delete comments on locked or confidential issues.
+- Fix milestone promotion authorization check.
+- Monkey kubeclient to not follow any redirects.
+- Configure mermaid to not render HTML content in diagrams.
+- Redact confidential events in the API.
+- Fix xss vulnerability sourced from package.json.
+- Fix a possible symlink time of check to time of use race condition in GitLab Pages.
+- Removed ability to see private group names when the group id is entered in the url.
+- Fix stored XSS for Environments.
+- Block loopback addresses in UrlBlocker.
+- Prevent SSRF attacks in HipChat integration.
+- Validate Wiki attachments are valid temporary files.
+
+
## 11.3.10 (2018-11-18)
### Security (1 change)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2dc8ac40dd4..4304f6c8744 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -181,4 +181,4 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
## Style guides
-This [documentation](doc/development/contributing/design.md) has been moved.
+This [documentation](doc/development/contributing/style_guides.md) has been moved.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9084fa2f716..bd8bf882d06 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.1.0
+1.7.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index f0bb29e7638..3a3cd8cc8b0 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.3.0
+1.3.1
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 6da4de57dc6..016dac34bf9 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-8.4.1
+8.4.3
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 21c8c7b46b8..18bb4182dd0 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-7.1.1
+7.5.0
diff --git a/Gemfile b/Gemfile
index 524d9d6a1de..93c2052f15f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,8 +5,13 @@ 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
+# `config.ru`. This can be removed once a new update for Rack
+# is available that contains https://github.com/rack/rack/pull/1201.
+gem_versions['rack'] = rails5? ? '2.0.6' : '1.6.11'
# --- The end of special code for migrating to Rails 5.0 ---
source 'https://rubygems.org'
@@ -77,7 +82,7 @@ gem 'validates_hostname', '~> 1.0.6'
gem 'browser', '~> 2.5'
# GPG
-gem 'gpgme'
+gem 'gpgme', '~> 2.0.18'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -86,7 +91,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap'
# API
-gem 'grape', '~> 1.1'
+gem 'grape', '~> 1.1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
@@ -154,6 +159,8 @@ gem 'icalendar'
gem 'diffy', '~> 3.1.0'
# Application server
+gem 'rack', gem_versions['rack']
+
group :unicorn do
gem 'unicorn', '~> 5.1.0'
gem 'unicorn-worker-killer', '~> 0.4.4'
@@ -256,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'
@@ -291,7 +301,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.3'
gem 'gettext', '~> 3.2.2', require: false, group: :development
-gem 'batch-loader', '~> 1.2.1'
+gem 'batch-loader', '~> 1.2.2'
# Perf bar
gem 'peek', '~> 1.0.1'
@@ -425,7 +435,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.1.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 f622c6292b2..608d1814127 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -73,7 +73,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.2.1)
+ batch-loader (1.2.2)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -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.1.0)
+ gitaly-proto (1.3.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -313,8 +314,8 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
- gpgme (2.0.13)
- mini_portile2 (~> 2.1)
+ gpgme (2.0.18)
+ mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
builder
@@ -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)
@@ -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)
@@ -950,7 +951,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader (~> 1.2.1)
+ batch-loader (~> 1.2.2)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
@@ -1006,7 +1007,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.1.0)
+ gitaly-proto (~> 1.3.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5)
@@ -1016,8 +1017,8 @@ DEPENDENCIES
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.6)
- gpgme
- grape (~> 1.1)
+ gpgme (~> 2.0.18)
+ grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
@@ -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)
@@ -1088,6 +1090,7 @@ DEPENDENCIES
pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
+ rack (= 2.0.6)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock
index 2542e085815..9e7bae84299 100644
--- a/Gemfile.rails4.lock
+++ b/Gemfile.rails4.lock
@@ -70,7 +70,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.2.1)
+ batch-loader (1.2.2)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -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.1.0)
+ gitaly-proto (1.3.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.5)
@@ -310,8 +311,8 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
- gpgme (2.0.13)
- mini_portile2 (~> 2.1)
+ gpgme (2.0.18)
+ mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
builder
@@ -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)
@@ -941,7 +942,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader (~> 1.2.1)
+ batch-loader (~> 1.2.2)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
@@ -998,7 +999,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.1.0)
+ gitaly-proto (~> 1.3.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
@@ -1007,8 +1008,8 @@ DEPENDENCIES
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.6)
- gpgme
- grape (~> 1.1)
+ gpgme (~> 2.0.18)
+ grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
@@ -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)
@@ -1079,11 +1081,12 @@ DEPENDENCIES
pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
+ rack (= 1.6.11)
rack-attack (~> 4.4.1)
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)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 3f7a1ef1bfc..de003e70e61 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,15 +5,17 @@ import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
+ subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
- mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ 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',
- mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
- mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
@@ -99,36 +101,45 @@ const Api = {
},
// Return Merge Request for project
- mergeRequest(projectPath, mergeRequestId, params = {}) {
- const url = Api.buildUrl(Api.mergeRequestPath)
+ projectMergeRequest(projectPath, mergeRequestId, params = {}) {
+ const url = Api.buildUrl(Api.projectMergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url, { params });
},
- mergeRequests(params = {}) {
- const url = Api.buildUrl(Api.mergeRequestsPath);
-
- return axios.get(url, { params });
- },
-
- mergeRequestChanges(projectPath, mergeRequestId) {
- const url = Api.buildUrl(Api.mergeRequestChangesPath)
+ projectMergeRequestChanges(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.projectMergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
- mergeRequestVersions(projectPath, mergeRequestId) {
- const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+ projectMergeRequestVersions(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.projectMergeRequestVersionsPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
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);
+
+ return axios.get(url, { params });
+ },
+
newLabel(namespacePath, projectPath, data, callback) {
let url;
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 720f30e18e6..35380ca49fb 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -26,6 +26,9 @@ export default function renderMermaid($els) {
},
// mermaidAPI options
theme: 'neutral',
+ flowchart: {
+ htmlLabels: false,
+ },
});
$els.each((i, el) => {
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/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 a37cb4def28..665a9c77822 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -296,7 +296,6 @@ export default {
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<div slot="description" v-html="certManagerDescription"></div>
@@ -396,6 +395,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
@@ -405,17 +405,15 @@ 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">
<p>
{{
- s__(`ClusterIntegration|Knative (pronounced kay-nay-tiv) extends
- Kubernetes to provide a set of middleware components that are
- essential to build modern, source-centric, and container-based
- applications that can run anywhere: on premises, in the cloud, or
- even in a third-party data center.`)
+ s__(`ClusterIntegration|Knative extends Kubernetes to provide
+ a set of middleware components that are essential to build modern,
+ source-centric, and container-based applications that can run
+ anywhere: on premises, in the cloud, or even in a third-party data center.`)
}}
</p>
@@ -433,7 +431,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/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 22da38ce7a5..bf9244df7f7 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -102,6 +102,12 @@ export default {
if (this.shouldShow) {
this.fetchData();
}
+
+ const id = window && window.location && window.location.hash;
+
+ if (id) {
+ this.setHighlightedRow(id.slice(1));
+ }
},
created() {
this.adjustView();
@@ -114,6 +120,7 @@ export default {
'fetchDiffFiles',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
+ 'setHighlightedRow',
]),
fetchData() {
this.fetchDiffFiles()
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..3b2a0d156ca 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -52,7 +52,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() {
@@ -143,9 +145,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 +164,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/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index b969017a2bb..0c0a0faa59d 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -56,9 +56,12 @@ export default {
return `${noteData.author.name}: ${note}`;
},
toggleDiscussions() {
+ const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
+
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
+ forceExpanded,
});
});
},
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index c02561b7599..c0613d80d37 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -72,6 +72,13 @@ export default {
diffFiles: state => state.diffs.diffFiles,
}),
...mapGetters(['isLoggedIn']),
+ lineCode() {
+ return (
+ this.line.line_code ||
+ (this.line.left && this.line.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
lineHref() {
return `#${this.line.line_code || ''}`;
},
@@ -97,9 +104,9 @@ export default {
},
},
methods: {
- ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
+ ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
handleCommentButton() {
- this.showCommentForm({ lineCode: this.line.line_code });
+ this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
handleLoadMoreLines() {
if (this.isRequesting) {
@@ -160,7 +167,7 @@ export default {
>
<template v-else>
<button
- v-if="shouldShowCommentButton"
+ v-show="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
@@ -168,7 +175,13 @@ export default {
>
<icon :size="12" name="comment" />
</button>
- <a v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref"> </a>
+ <a
+ v-if="lineNumber"
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode);"
+ >
+ </a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
</template>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index c7cef74fe40..9fd02acbd6e 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -73,6 +73,7 @@ export default {
this.cancelCommentForm({
lineCode: this.line.line_code,
+ fileHash: this.diffFileHash,
});
this.$nextTick(() => {
this.resetAutoSave();
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index f4eb956adcb..d174b13e133 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
import DiffLineGutterContent from './diff_line_gutter_content.vue';
import {
MATCH_LINE_TYPE,
@@ -30,6 +30,11 @@ export default {
type: String,
required: true,
},
+ isHighlighted: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
diffViewType: {
type: String,
required: false,
@@ -85,6 +90,7 @@ export default {
const { type } = this.line;
return {
+ hll: this.isHighlighted,
[type]: type,
[LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
[LINE_HOVER_CLASS_NAME]:
@@ -99,6 +105,7 @@ export default {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
},
+ methods: mapActions('diffs', ['setHighlightedRow']),
};
</script>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 91b87fb042c..aa40b24950a 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@@ -17,29 +16,31 @@ export default {
type: String,
required: true,
},
- lineIndex: {
- type: Number,
- required: true,
- },
},
computed: {
- ...mapState({
- diffLineCommentForms: state => state.diffs.diffLineCommentForms,
- }),
className() {
return this.line.discussions.length ? '' : 'js-temp-notes-holder';
},
+ shouldRender() {
+ if (this.line.hasForm) return true;
+
+ if (!this.line.discussions || !this.line.discussions.length) {
+ return false;
+ }
+
+ return this.line.discussions.every(discussion => discussion.expanded);
+ },
},
};
</script>
<template>
- <tr :class="className" class="notes_holder">
+ <tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes_content" colspan="3">
<div class="content">
<diff-discussions v-if="line.discussions.length" :discussions="line.discussions" />
<diff-line-note-form
- v-if="diffLineCommentForms[line.line_code]"
+ v-if="line.hasForm"
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 8d53fbded73..c764cbeb8e0 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
+import { mapGetters, mapActions, mapState } from 'vuex';
import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
@@ -40,6 +40,11 @@ export default {
};
},
computed: {
+ ...mapState({
+ isHighlighted(state) {
+ return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow;
+ },
+ }),
...mapGetters('diffs', ['isInlineView']),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
@@ -91,6 +96,7 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
+ :is-highlighted="isHighlighted"
class="diff-line-num old_line"
/>
<diff-table-cell
@@ -100,8 +106,18 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
+ :is-highlighted="isHighlighted"
class="diff-line-num new_line qa-new-diff-line"
/>
- <td :class="line.type" class="line_content" v-html="line.rich_text"></td>
+ <td
+ :class="[
+ line.type,
+ {
+ hll: isHighlighted,
+ },
+ ]"
+ class="line_content"
+ v-html="line.rich_text"
+ ></td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index fafc1649ce7..6a0ce760e6d 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { mapGetters } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
@@ -19,23 +19,18 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
- ...mapState({
- diffLineCommentForms: state => state.diffs.diffLineCommentForms,
- }),
+ ...mapGetters('diffs', ['commitId']),
diffLinesLength() {
return this.diffLines.length;
},
- userColorScheme() {
- return window.gon.user_color_scheme;
- },
},
+ userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<table
- :class="userColorScheme"
+ :class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"
>
@@ -49,11 +44,9 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-comment-row
- v-if="shouldRenderInlineCommentRow(line)"
- :key="index"
+ :key="`icr-${index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
- :line-index="index"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index c6b50983277..b98463d3dd3 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@@ -23,22 +22,13 @@ export default {
},
},
computed: {
- ...mapState({
- diffLineCommentForms: state => state.diffs.diffLineCommentForms,
- }),
- leftLineCode() {
- return this.line.left && this.line.left.line_code;
- },
- rightLineCode() {
- return this.line.right && this.line.right.line_code;
- },
hasExpandedDiscussionOnLeft() {
- return this.line.left && this.line.left.discussions
+ return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded)
: false;
},
hasExpandedDiscussionOnRight() {
- return this.line.right && this.line.right.discussions
+ return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded)
: false;
},
@@ -57,9 +47,10 @@ export default {
);
},
showRightSideCommentForm() {
- return (
- this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
- );
+ return this.line.right && this.line.right.type && this.line.right.hasForm;
+ },
+ showLeftSideCommentForm() {
+ return this.line.left && this.line.left.hasForm;
},
className() {
return (this.left && this.line.left.discussions.length > 0) ||
@@ -67,12 +58,30 @@ export default {
? ''
: 'js-temp-notes-holder';
},
+ shouldRender() {
+ const { line } = this;
+ const hasDiscussion =
+ (line.left && line.left.discussions && line.left.discussions.length) ||
+ (line.right && line.right.discussions && line.right.discussions.length);
+
+ if (
+ hasDiscussion &&
+ (this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight)
+ ) {
+ return true;
+ }
+
+ const hasCommentFormOnLeft = line.left && line.left.hasForm;
+ const hasCommentFormOnRight = line.right && line.right.hasForm;
+
+ return hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
},
};
</script>
<template>
- <tr :class="className" class="notes_holder">
+ <tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes_content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
@@ -81,7 +90,7 @@ export default {
/>
</div>
<diff-line-note-form
- v-if="diffLineCommentForms[leftLineCode]"
+ v-if="showLeftSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index 248dfd9815e..caf0df8a4e3 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import $ from 'jquery';
import DiffTableCell from './diff_table_cell.vue';
import {
@@ -43,6 +43,15 @@ export default {
};
},
computed: {
+ ...mapState({
+ isHighlighted(state) {
+ const lineCode =
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code);
+
+ return lineCode ? lineCode === state.diffs.highlightedRow : false;
+ },
+ }),
isContextLine() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
},
@@ -57,7 +66,14 @@ export default {
return OLD_NO_NEW_LINE_TYPE;
}
- return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
+ const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
+
+ return [
+ lineTypeClass,
+ {
+ hll: this.isHighlighted,
+ },
+ ];
},
},
created() {
@@ -114,6 +130,7 @@ export default {
:line-type="oldLineType"
:is-bottom="isBottom"
:is-hover="isLeftHover"
+ :is-highlighted="isHighlighted"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
line-position="left"
@@ -139,6 +156,7 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isRightHover"
+ :is-highlighted="isHighlighted"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
line-position="right"
@@ -146,7 +164,12 @@ export default {
/>
<td
:id="line.right.line_code"
- :class="line.right.type"
+ :class="[
+ line.right.type,
+ {
+ hll: isHighlighted,
+ },
+ ]"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
v-html="line.right.rich_text"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 771b8a80352..9a6e0e82529 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapGetters } from 'vuex';
+import { mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
@@ -19,23 +19,18 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
- ...mapState({
- diffLineCommentForms: state => state.diffs.diffLineCommentForms,
- }),
+ ...mapGetters('diffs', ['commitId']),
diffLinesLength() {
return this.diffLines.length;
},
- userColorScheme() {
- return window.gon.user_color_scheme;
- },
},
+ userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div
- :class="userColorScheme"
+ :class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file"
>
@@ -50,7 +45,6 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<parallel-diff-comment-row
- v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
:diff-file-hash="diffFile.file_hash"
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index a3de058b20e..952963e0711 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -33,6 +33,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash);
};
+export const setHighlightedRow = ({ commit }, lineCode) => {
+ commit(types.SET_HIGHLIGHTED_ROW, lineCode);
+};
+
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
export const assignDiscussionsToDiff = (
@@ -99,12 +103,12 @@ export const setParallelDiffViewType = ({ commit }) => {
historyPushState(url);
};
-export const showCommentForm = ({ commit }, params) => {
- commit(types.ADD_COMMENT_FORM_LINE, params);
+export const showCommentForm = ({ commit }, { lineCode, fileHash }) => {
+ commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: true });
};
-export const cancelCommentForm = ({ commit }, params) => {
- commit(types.REMOVE_COMMENT_FORM_LINE, params);
+export const cancelCommentForm = ({ commit }, { lineCode, fileHash }) => {
+ commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: false });
};
export const loadMoreLines = ({ commit }, options) => {
@@ -127,7 +131,7 @@ export const loadMoreLines = ({ commit }, options) => {
export const scrollToLineIfNeededInline = (_, line) => {
const hash = getLocationHash();
- if (hash && line.lineCode === hash) {
+ if (hash && line.line_code === hash) {
handleLocationHash();
}
};
@@ -137,19 +141,25 @@ export const scrollToLineIfNeededParallel = (_, line) => {
if (
hash &&
- ((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
+ ((line.left && line.left.line_code === hash) || (line.right && line.right.line_code === hash))
) {
handleLocationHash();
}
};
-export const loadCollapsedDiff = ({ commit }, file) =>
- axios.get(file.loadCollapsedDiffUrl).then(res => {
- commit(types.ADD_COLLAPSED_DIFFS, {
- file,
- data: res.data,
+export const loadCollapsedDiff = ({ commit, getters }, file) =>
+ axios
+ .get(file.load_collapsed_diff_url, {
+ params: {
+ commit_id: getters.commitId,
+ },
+ })
+ .then(res => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
+ });
});
- });
export const expandAllFiles = ({ commit }) => {
commit(types.EXPAND_ALL_FILES);
@@ -182,8 +192,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,
});
@@ -191,8 +202,8 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
+ .then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
- .then(() => dispatch('startTaskList', null, { root: true }))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 6a87b712b48..fdf1efbb10e 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -70,40 +70,6 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
) || [];
-export const shouldRenderParallelCommentRow = state => line => {
- const hasDiscussion =
- (line.left && line.left.discussions && line.left.discussions.length) ||
- (line.right && line.right.discussions && line.right.discussions.length);
-
- const hasExpandedDiscussionOnLeft =
- line.left && line.left.discussions && line.left.discussions.length
- ? line.left.discussions.every(discussion => discussion.expanded)
- : false;
- const hasExpandedDiscussionOnRight =
- line.right && line.right.discussions && line.right.discussions.length
- ? line.right.discussions.every(discussion => discussion.expanded)
- : false;
-
- if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
- return true;
- }
-
- const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.line_code];
- const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.line_code];
-
- return hasCommentFormOnLeft || hasCommentFormOnRight;
-};
-
-export const shouldRenderInlineCommentRow = state => line => {
- if (state.diffLineCommentForms[line.line_code]) return true;
-
- if (!line.discussions || line.discussions.length === 0) {
- return false;
- }
-
- return line.discussions.every(discussion => discussion.expanded);
-};
-
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 2f59a3822f4..98e57d52d77 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -18,7 +18,6 @@ export default () => ({
diffFiles: [],
mergeRequestDiffs: [],
mergeRequestDiff: null,
- diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
@@ -27,4 +26,5 @@ export default () => ({
currentDiffFileId: '',
projectPath: '',
commentForms: [],
+ highlightedRow: null,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index e011031e72c..0338cde3658 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -3,8 +3,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_DIFF_DATA = 'SET_DIFF_DATA';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
-export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
-export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
+export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
@@ -18,3 +17,4 @@ export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
+export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 2133cfe4825..331fb052371 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { sortTree } from '~/ide/stores/utils';
import {
@@ -49,12 +48,30 @@ export default {
Object.assign(state, { diffViewType });
},
- [types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
- Vue.set(state.diffLineCommentForms, lineCode, true);
- },
+ [types.TOGGLE_LINE_HAS_FORM](state, { lineCode, fileHash, hasForm }) {
+ const diffFile = state.diffFiles.find(f => f.file_hash === fileHash);
+
+ if (!diffFile) return;
+
+ if (diffFile.highlighted_diff_lines) {
+ diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
+ }
+
+ if (diffFile.parallel_diff_lines) {
+ const line = diffFile.parallel_diff_lines.find(l => {
+ const { left, right } = l;
+
+ return (left && left.line_code === lineCode) || (right && right.line_code === lineCode);
+ });
+
+ if (line.left && line.left.line_code === lineCode) {
+ line.left.hasForm = hasForm;
+ }
- [types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
- Vue.delete(state.diffLineCommentForms, lineCode);
+ if (line.right && line.right.line_code === lineCode) {
+ line.right.hasForm = hasForm;
+ }
+ }
},
[types.ADD_CONTEXT_LINES](state, options) {
@@ -68,6 +85,7 @@ export default {
...line,
line_code: line.line_code || `${fileHash}_${line.old_line}_${line.new_line}`,
discussions: line.discussions || [],
+ hasForm: false,
}));
addContextLines({
@@ -112,7 +130,7 @@ export default {
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
- if (lineCheck(line)) {
+ if (!line.discussions.some(({ id }) => discussion.id === id) && lineCheck(line)) {
return {
...line,
discussions: line.discussions.concat(discussion),
@@ -132,11 +150,17 @@ export default {
return {
left: {
...line.left,
- discussions: left ? line.left.discussions.concat(discussion) : [],
+ 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.concat(discussion) : [],
+ discussions:
+ right && !left && !line.right.discussions.some(({ id }) => id === discussion.id)
+ ? line.right.discussions.concat(discussion)
+ : (line.right && line.right.discussions) || [],
},
};
}
@@ -223,4 +247,7 @@ export default {
[types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
},
+ [types.SET_HIGHLIGHTED_ROW](state, lineCode) {
+ state.highlightedRow = lineCode;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index d9d3c0f2ca2..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
@@ -209,9 +210,11 @@ export function prepareDiffData(diffData) {
const line = file.parallel_diff_lines[u];
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
+ line.left.hasForm = false;
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
+ line.right.hasForm = false;
}
}
}
@@ -220,7 +223,7 @@ export function prepareDiffData(diffData) {
const linesLength = file.highlighted_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.highlighted_diff_lines[u];
- Object.assign(line, { ...trimFirstCharOfLineContent(line) });
+ Object.assign(line, { ...trimFirstCharOfLineContent(line), hasForm: false });
}
showingLines += file.parallel_diff_lines.length;
}
@@ -322,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/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index bb0ecb8efe7..b494b7e2de0 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -88,11 +88,16 @@ export const conditions = [
value: 'started',
},
{
- url: 'label_name[]=No+Label',
+ url: 'label_name[]=None',
tokenKey: 'label',
value: 'none',
},
{
+ url: 'label_name[]=Any',
+ tokenKey: 'any',
+ value: 'any',
+ },
+ {
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
value: 'none',
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index b4a3037c1b7..2049760fe29 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -10,13 +10,18 @@ export default function groupsSelect() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
+
$select.select2({
placeholder: 'Search for a group',
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
- url: Api.buildUrl(Api.groupsPath),
+ url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index e318367a5ec..7a57ccf2dd3 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -105,7 +105,7 @@ export default {
:key="tabView.name"
class="h-100"
>
- <component :is="tabView.name" />
+ <component :is="tabView.component || tabView.name" />
</div>
</resizable-panel>
<nav class="ide-activity-bar">
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/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index f0193d8e8ea..13449592e62 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -41,13 +41,13 @@ export default {
return Api.project(`${namespace}/${project}`);
},
getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
- return Api.mergeRequest(projectId, mergeRequestId, params);
+ return Api.projectMergeRequest(projectId, mergeRequestId, params);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
- return Api.mergeRequestChanges(projectId, mergeRequestId);
+ return Api.projectMergeRequestChanges(projectId, mergeRequestId);
},
getProjectMergeRequestVersions(projectId, mergeRequestId) {
- return Api.mergeRequestVersions(projectId, mergeRequestId);
+ return Api.projectMergeRequestVersions(projectId, mergeRequestId);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 4565c11a83f..8b5f7558654 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -23,13 +23,19 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }
export const receiveMergeRequestsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
-export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
+export const fetchMergeRequests = (
+ { dispatch, state: { state }, rootState: { currentProjectId } },
+ { type, search = '' },
+) => {
dispatch('requestMergeRequests');
dispatch('resetMergeRequests');
- const scope = type ? scopes[type] : 'all';
+ const scope = type && scopes[type];
+ const request = scope
+ ? Api.mergeRequests({ scope, state, search })
+ : Api.projectMergeRequest(currentProjectId, '', { state, search });
- return Api.mergeRequests({ scope, state, search })
+ return request
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index 2d09cf5760f..f7fbb9503a0 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -128,7 +128,7 @@ export default {
};
</script>
<template>
- <div class="prepend-top-default js-environment-container">
+ <div class="prepend-top-default append-bottom-default js-environment-container">
<div class="environment-information">
<ci-icon :status="iconStatus" />
<p class="inline append-bottom-0" v-html="environment"></p>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 7b077d5e621..ec52d272168 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -28,20 +28,22 @@ export default {
<div class="bs-callout bs-callout-warning">
<p v-if="tags.length" class="js-stuck-with-tags append-bottom-0">
{{
- s__(`This job is stuck, because you don't have
+ s__(`This job is stuck because you don't have
any active runners online with any of these tags assigned to them:`)
}}
- <span v-for="(tag, index) in tags" :key="index" class="badge badge-primary"> {{ tag }} </span>
+ <span v-for="(tag, index) in tags" :key="index" class="badge badge-primary append-right-4">
+ {{ tag }}
+ </span>
</p>
<p v-else-if="hasNoRunnersForProject" class="js-stuck-no-runners append-bottom-0">
{{
- s__(`Job|This job is stuck, because the project
+ s__(`Job|This job is stuck because the project
doesn't have any runners online assigned to it.`)
}}
</p>
<p v-else class="js-stuck-no-active-runner append-bottom-0">
{{
- s__(`This job is stuck, because you don't
+ s__(`This job is stuck because you don't
have any active runners that can run this job.`)
}}
</p>
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/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a282c2df441..9850f7ce782 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -17,27 +17,29 @@ export function getParameterValues(sParam) {
// @param {Object} params - url keys and value to merge
// @param {String} url
export function mergeUrlParams(params, url) {
- let newUrl = Object.keys(params).reduce((acc, paramName) => {
- const paramValue = encodeURIComponent(params[paramName]);
- const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
-
- if (paramValue === null) {
- return acc.replace(pattern, '');
- } else if (url.search(pattern) !== -1) {
- return acc.replace(pattern, `$1${paramValue}$2`);
- }
-
- return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
- }, decodeURIComponent(url));
+ const re = /^([^?#]*)(\?[^#]*)?(.*)/;
+ const merged = {};
+ const urlparts = url.match(re);
+
+ if (urlparts[2]) {
+ urlparts[2]
+ .substr(1)
+ .split('&')
+ .forEach(part => {
+ if (part.length) {
+ const kv = part.split('=');
+ merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
+ }
+ });
+ }
- // Remove a trailing ampersand
- const lastChar = newUrl[newUrl.length - 1];
+ Object.assign(merged, params);
- if (lastChar === '&') {
- newUrl = newUrl.slice(0, -1);
- }
+ const query = Object.keys(merged)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
+ .join('&');
- return newUrl;
+ return `${urlparts[1]}?${query}${urlparts[3]}`;
}
export function removeParamQueryString(url, param) {
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/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 0d8f31d6bfc..196b84621b6 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -30,6 +30,7 @@ export default class MirrorRepos {
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
this.initMirrorSSH();
+ this.updateProtectedBranches();
}
initMirrorSSH() {
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 815063237fc..64a1df80a8e 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -105,6 +105,9 @@ export default {
deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
+ shouldRenderData() {
+ return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
+ },
},
watch: {
hoverData() {
@@ -120,17 +123,17 @@ export default {
},
draw() {
const breakpointSize = bp.getBreakpointSize();
- const query = this.graphData.queries[0];
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
+
this.margin = measurements.large.margin;
+
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
- this.unitOfDisplay = query.unit || '';
+
this.yAxisLabel = this.graphData.y_label || 'Values';
- this.legendTitle = query.label || 'Average';
this.graphWidth = svgWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight - 50;
@@ -139,8 +142,15 @@ export default {
// pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = svgWidth / this.baseGraphWidth;
- this.renderAxesPaths();
- this.formatDeployments();
+ // set the legends on the axes
+ const [query] = this.graphData.queries;
+ this.legendTitle = query ? query.label : 'Average';
+ this.unitOfDisplay = query ? query.unit : '';
+
+ if (this.shouldRenderData) {
+ this.renderAxesPaths();
+ this.formatDeployments();
+ }
},
handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint();
@@ -266,7 +276,7 @@ export default {
:y-axis-label="yAxisLabel"
:unit-of-display="unitOfDisplay"
/>
- <svg ref="graphData" :viewBox="innerViewBox" class="graph-data">
+ <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
<graph-path
v-for="(path, index) in timeSeries"
@@ -293,8 +303,14 @@ export default {
@mousemove="handleMouseOverGraph($event);"
/>
</svg>
+ <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
+ <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
+ {{ s__('Metrics|No data to display') }}
+ </text>
+ </svg>
</svg>
<graph-flag
+ v-if="shouldRenderData"
:real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 176f7d9eef2..8692c873a41 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -7,10 +7,29 @@ function sortMetrics(metrics) {
.value();
}
+function checkQueryEmptyData(query) {
+ return {
+ ...query,
+ result: query.result.filter(timeSeries => {
+ const newTimeSeries = timeSeries;
+ const hasValue = series =>
+ !Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
+ const hasNonNullValue = timeSeries.values.find(hasValue);
+
+ newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
+
+ return newTimeSeries.values.length > 0;
+ }),
+ };
+}
+
+function removeTimeSeriesNoData(queries) {
+ return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
+}
+
function normalizeMetrics(metrics) {
- return metrics.map(metric => ({
- ...metric,
- queries: metric.queries.map(query => ({
+ return metrics.map(metric => {
+ const queries = metric.queries.map(query => ({
...query,
result: query.result.map(result => ({
...result,
@@ -19,8 +38,13 @@ function normalizeMetrics(metrics) {
value: Number(value),
})),
})),
- })),
- }));
+ }));
+
+ return {
+ ...metric,
+ queries: removeTimeSeriesNoData(queries),
+ };
+ });
}
export default class MonitoringStore {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 884ccca7bde..ce56beb1e6b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import Autosave from '../../autosave';
import {
@@ -30,6 +31,7 @@ export default {
markdownField,
userAvatarLink,
loadingButton,
+ TimelineEntryItem,
},
mixins: [issuableStateMixin],
props: {
@@ -245,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);
});
}
},
@@ -309,137 +315,135 @@ Please check your network connection and try again.`;
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
- <div v-else-if="canCreateNote" class="notes notes-form timeline">
- <div class="timeline-entry note-form">
- <div class="timeline-entry-inner">
- <div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon d-none d-sm-none d-md-block">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <div class="timeline-content timeline-content-form">
- <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
- <div class="error-alert"></div>
+ <ul v-else-if="canCreateNote" class="notes notes-form timeline">
+ <timeline-entry-item class="note-form">
+ <div class="flash-container error-alert timeline-content"></div>
+ <div class="timeline-icon d-none d-sm-none d-md-block">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content timeline-content-form">
+ <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
+ <div class="error-alert"></div>
- <issue-warning
- v-if="hasWarning(getNoteableData)"
- :is-locked="isLocked(getNoteableData)"
- :is-confidential="isConfidential(getNoteableData)"
- />
+ <issue-warning
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
+ />
- <markdown-field
- ref="markdownField"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
- :markdown-version="markdownVersion"
- :add-spacing-classes="false"
- >
- <textarea
- id="note-body"
- ref="textarea"
- slot="textarea"
- v-model="note"
- :disabled="isSubmitting"
- name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text
+ <markdown-field
+ ref="markdownField"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :markdown-version="markdownVersion"
+ :add-spacing-classes="false"
+ >
+ <textarea
+ id="note-body"
+ ref="textarea"
+ slot="textarea"
+ v-model="note"
+ :disabled="isSubmitting"
+ name="note[note]"
+ class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
- data-supports-quick-actions="true"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
- @keydown.up="editCurrentUserLastNote();"
- @keydown.meta.enter="handleSave();"
- @keydown.ctrl.enter="handleSave();"
- >
- </textarea>
- </markdown-field>
- <div class="note-form-actions">
- <div
- class="float-left btn-group
+ data-supports-quick-actions="true"
+ aria-label="Description"
+ placeholder="Write a comment or drag your files here…"
+ @keydown.up="editCurrentUserLastNote();"
+ @keydown.meta.enter="handleSave();"
+ @keydown.ctrl.enter="handleSave();"
+ >
+ </textarea>
+ </markdown-field>
+ <div class="note-form-actions">
+ <div
+ class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
- >
- <button
- :disabled="isSubmitButtonDisabled"
- class="btn btn-create comment-btn js-comment-button js-comment-submit-button
+ >
+ <button
+ :disabled="isSubmitButtonDisabled"
+ class="btn btn-create comment-btn js-comment-button js-comment-submit-button
qa-comment-button"
- type="submit"
- @click.prevent="handleSave();"
- >
- {{ __(commentButtonTitle) }}
- </button>
- <button
- :disabled="isSubmitButtonDisabled"
- name="button"
- type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
- data-display="static"
- data-toggle="dropdown"
- aria-label="Open comment type dropdown"
- >
- <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
- </button>
-
- <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
- <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
- <button
- type="button"
- class="btn btn-transparent"
- @click.prevent="setNoteType('comment');"
- >
- <i aria-hidden="true" class="fa fa-check icon"> </i>
- <div class="description">
- <strong>Comment</strong>
- <p>Add a general comment to this {{ noteableDisplayName }}.</p>
- </div>
- </button>
- </li>
- <li class="divider droplab-item-ignore"></li>
- <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
- <button
- type="button"
- class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion');"
- >
- <i aria-hidden="true" class="fa fa-check icon"> </i>
- <div class="description">
- <strong>Start discussion</strong>
- <p>{{ startDiscussionDescription }}</p>
- </div>
- </button>
- </li>
- </ul>
- </div>
-
- <loading-button
- v-if="canUpdateIssue"
- :loading="isToggleStateButtonLoading"
- :container-class="[
- actionButtonClassNames,
- 'btn btn-comment btn-comment-and-close js-action-button',
- ]"
- :disabled="isToggleStateButtonLoading || isSubmitting"
- :label="issueActionButtonTitle"
- @click="handleSave(true);"
- />
-
+ type="submit"
+ @click.prevent="handleSave();"
+ >
+ {{ __(commentButtonTitle) }}
+ </button>
<button
- v-if="note.length"
+ :disabled="isSubmitButtonDisabled"
+ name="button"
type="button"
- class="btn btn-cancel js-note-discard"
- @click="discard"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ data-display="static"
+ data-toggle="dropdown"
+ aria-label="Open comment type dropdown"
>
- Discard draft
+ <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
</button>
+
+ <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
+ <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
+ <button
+ type="button"
+ class="btn btn-transparent"
+ @click.prevent="setNoteType('comment');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Comment</strong>
+ <p>Add a general comment to this {{ noteableDisplayName }}.</p>
+ </div>
+ </button>
+ </li>
+ <li class="divider droplab-item-ignore"></li>
+ <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
+ <button
+ type="button"
+ class="btn btn-transparent qa-discussion-option"
+ @click.prevent="setNoteType('discussion');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Start discussion</strong>
+ <p>{{ startDiscussionDescription }}</p>
+ </div>
+ </button>
+ </li>
+ </ul>
</div>
- </form>
- </div>
+
+ <loading-button
+ v-if="canUpdateIssue"
+ :loading="isToggleStateButtonLoading"
+ :container-class="[
+ actionButtonClassNames,
+ 'btn btn-comment btn-comment-and-close js-action-button',
+ ]"
+ :disabled="isToggleStateButtonLoading || isSubmitting"
+ :label="issueActionButtonTitle"
+ @click="handleSave(true);"
+ />
+
+ <button
+ v-if="note.length"
+ type="button"
+ class="btn btn-cancel js-note-discard"
+ @click="discard"
+ >
+ Discard draft
+ </button>
+ </div>
+ </form>
</div>
- </div>
- </div>
+ </timeline-entry-item>
+ </ul>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 8e8bd150647..af821df0fd2 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -4,7 +4,9 @@ import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
-import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
+import { getDiffMode } from '~/diffs/store/utils';
+
+const FIRST_CHAR_REGEX = /^(\+|-| )/;
export default {
components: {
@@ -26,46 +28,16 @@ export default {
},
computed: {
...mapState({
- noteableData: state => state.notes.noteableData,
projectPath: state => state.diffs.projectPath,
}),
diffMode() {
- return getDiffMode(this.diffFile);
+ return getDiffMode(this.discussion.diff_file);
},
hasTruncatedDiffLines() {
return (
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
);
},
- isDiscussionsExpanded() {
- return true; // TODO: @fatihacet - Fix this.
- },
- isCollapsed() {
- return this.diffFile.collapsed || false;
- },
- isImageDiff() {
- return !this.diffFile.text;
- },
- diffFileClass() {
- const { text } = this.diffFile;
- return text ? 'text-file' : 'js-image-file';
- },
- diffFile() {
- return this.discussion.diff_file;
- },
- imageDiffHtml() {
- return this.discussion.image_diff_html;
- },
- userColorScheme() {
- return window.gon.user_color_scheme;
- },
- normalizedDiffLines() {
- if (this.discussion.truncated_diff_lines) {
- return this.discussion.truncated_diff_lines.map(line => trimFirstCharOfLineContent(line));
- }
-
- return [];
- },
},
mounted() {
if (!this.hasTruncatedDiffLines) {
@@ -74,9 +46,6 @@ export default {
},
methods: {
...mapActions(['fetchDiscussionDiffLines']),
- rowTag(html) {
- return html.outerHTML ? 'tr' : 'template';
- },
fetchDiff() {
this.error = false;
this.fetchDiscussionDiffLines(this.discussion)
@@ -85,31 +54,45 @@ export default {
this.error = true;
});
},
+ trimChar(line) {
+ return line.replace(FIRST_CHAR_REGEX, '');
+ },
},
+ userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
<template>
- <div ref="fileHolder" :class="diffFileClass" class="diff-file file-holder">
+ <div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder">
<diff-file-header
:discussion-path="discussion.discussion_path"
- :diff-file="diffFile"
+ :diff-file="discussion.diff_file"
:can-current-user-fork="false"
- :discussions-expanded="isDiscussionsExpanded"
- :expanded="!isCollapsed"
+ :expanded="!discussion.diff_file.collapsed"
/>
- <div v-if="diffFile.text" :class="userColorScheme" class="diff-content code">
+ <div
+ v-if="discussion.diff_file.text"
+ :class="$options.userColorSchemeClass"
+ class="diff-content code"
+ >
<table>
- <tr v-for="line in normalizedDiffLines" :key="line.line_code" class="line_holder">
- <td class="diff-line-num old_line">{{ line.old_line }}</td>
- <td class="diff-line-num new_line">{{ line.new_line }}</td>
- <td :class="line.type" class="line_content" v-html="line.rich_text"></td>
- </tr>
+ <template v-if="hasTruncatedDiffLines">
+ <tr
+ v-for="line in discussion.truncated_diff_lines"
+ v-once
+ :key="line.line_code"
+ class="line_holder"
+ >
+ <td class="diff-line-num old_line">{{ line.old_line }}</td>
+ <td class="diff-line-num new_line">{{ line.new_line }}</td>
+ <td :class="line.type" class="line_content" v-html="trimChar(line.rich_text)"></td>
+ </tr>
+ </template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">
<td class="old_line diff-line-num"></td>
<td class="new_line diff-line-num"></td>
<td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
- Unable to load the diff
+ {{ error }} Unable to load the diff
<button
class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
@@ -131,17 +114,17 @@ export default {
<div v-else>
<diff-viewer
:diff-mode="diffMode"
- :new-path="diffFile.new_path"
- :new-sha="diffFile.diff_refs.head_sha"
- :old-path="diffFile.old_path"
- :old-sha="diffFile.diff_refs.base_sha"
- :file-hash="diffFile.file_hash"
+ :new-path="discussion.diff_file.new_path"
+ :new-sha="discussion.diff_file.diff_refs.head_sha"
+ :old-path="discussion.diff_file.old_path"
+ :old-sha="discussion.diff_file.diff_refs.base_sha"
+ :file-hash="discussion.diff_file.file_hash"
:project-path="projectPath"
>
<image-diff-overlay
slot="image-overlay"
:discussions="discussion"
- :file-hash="diffFile.file_hash"
+ :file-hash="discussion.diff_file.file_hash"
:show-comment-icon="true"
:should-toggle-discussion="false"
badge-class="image-comment-badge"
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index ee79ecbf9b3..c7cfc0f0f3b 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,13 +1,12 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
@@ -17,9 +16,9 @@ export default {
...mapGetters([
'getUserData',
'getNoteableData',
- 'discussionCount',
+ 'resolvableDiscussionsCount',
'firstUnresolvedDiscussionId',
- 'resolvedDiscussionCount',
+ 'unresolvedDiscussionsCount',
]),
isLoggedIn() {
return this.getUserData.id;
@@ -27,15 +26,15 @@ export default {
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
- countText() {
- return pluralize('discussion', this.discussionCount);
- },
allResolved() {
- return this.resolvedDiscussionCount === this.discussionCount;
+ return this.unresolvedDiscussionsCount === 0;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
+ resolvedDiscussionsCount() {
+ return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
+ },
},
methods: {
...mapActions(['expandDiscussion']),
@@ -50,7 +49,7 @@ export default {
</script>
<template>
- <div v-if="discussionCount > 0" class="line-resolve-all-container prepend-top-8">
+ <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8">
<div>
<div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all">
<span
@@ -61,15 +60,15 @@ export default {
<icon name="check-circle" />
</span>
<span class="line-resolve-text">
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
+ {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
+ {{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }}
</span>
</div>
<div v-if="resolveAllDiscussionsIssuePath && !allResolved" class="btn-group" role="group">
<a
- v-tooltip
+ v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
:title="s__('Resolve all discussions in new issue')"
- data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<icon name="issue-new" />
@@ -77,9 +76,8 @@ export default {
</div>
<div v-if="isLoggedIn && !allResolved" class="btn-group" role="group">
<button
- v-tooltip
+ v-gl-tooltip
title="Jump to first unresolved discussion"
- data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion"
>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 9a5817890c9..d99694b06e9 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,8 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'NoteActions',
@@ -11,7 +10,7 @@ export default {
GlLoadingIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
authorId: {
@@ -119,10 +118,10 @@ export default {
<template>
<div class="note-actions">
- <span v-if="accessLevel" class="note-role user-access-role"> {{ accessLevel }} </span>
+ <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
<div v-if="canResolve" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
@@ -138,12 +137,10 @@ export default {
</div>
<div v-if="canAwardEmoji" class="note-actions-item">
<a
- v-tooltip
+ v-gl-tooltip.bottom
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button js-add-award js-note-emoji"
data-position="right"
- data-placement="bottom"
- data-container="body"
href="#"
title="Add reaction"
>
@@ -158,12 +155,10 @@ export default {
</div>
<div v-if="canEdit" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
- data-container="body"
- data-placement="bottom"
@click="onEdit"
>
<icon name="pencil" css-classes="link-highlight" />
@@ -171,12 +166,10 @@ export default {
</div>
<div v-if="showDeleteAction" class="note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
- data-container="body"
- data-placement="bottom"
@click="onDelete"
>
<icon name="remove" class="link-highlight" />
@@ -184,19 +177,17 @@ export default {
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
<button
- v-tooltip
+ v-gl-tooltip.bottom
type="button"
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
data-toggle="dropdown"
- data-container="body"
- data-placement="bottom"
>
<icon css-classes="icon" name="ellipsis_v" />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
- <a :href="reportAbusePath"> {{ __('Report abuse to GitLab') }} </a>
+ <a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a>
</li>
<li v-if="noteUrl">
<button
@@ -213,7 +204,7 @@ export default {
type="button"
@click.prevent="onDelete"
>
- <span class="text-danger"> {{ __('Delete comment') }} </span>
+ <span class="text-danger">{{ __('Delete comment') }}</span>
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 4aba2e65edb..3d60eb02db8 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
awards: {
@@ -167,21 +167,19 @@ export default {
<button
v-for="(awardList, awardName, index) in groupedAwards"
:key="index"
- v-tooltip
+ v-gl-tooltip.bottom="{ boundary: 'viewport' }"
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
class="btn award-control"
- data-boundary="viewport"
- data-placement="bottom"
type="button"
@click="handleAward(awardName);"
>
<span v-html="getAwardHTML(awardName)"></span>
- <span class="award-control-text js-counter"> {{ awardList.length }} </span>
+ <span class="award-control-text js-counter">{{ awardList.length }}</span>
</button>
<div v-if="canAwardEmoji" class="award-menu-holder">
<button
- v-tooltip
+ v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByMe }"
class="award-control btn js-add-award"
title="Add reaction"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index ad58267b533..95164183ccb 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -48,13 +48,19 @@ export default {
required: false,
default: '',
},
+ resolveDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
- isResolving: false,
+ isResolving: this.resolveDiscussion,
+ isUnresolving: !this.resolveDiscussion,
resolveAsThread: true,
};
},
@@ -149,7 +155,7 @@ export default {
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
<div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a :href="noteHash" target="_blank" rel="noopener noreferrer"> updated comment </a> to ensure
+ <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure
information is not lost.
</div>
<div class="flash-container timeline-content"></div>
@@ -174,22 +180,20 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text
-js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate();"
@keydown.ctrl.enter="handleUpdate();"
@keydown.up="editMyLastNote();"
@keydown.esc="cancelHandler(true);"
- >
- </textarea>
+ ></textarea>
</markdown-field>
<div class="note-form-actions clearfix">
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-success js-comment-button "
+ class="js-vue-issue-save btn btn-success js-comment-button"
@click="handleUpdate();"
>
{{ saveButtonTitle }}
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 8b7450783c9..e1a58e7cb26 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -73,7 +73,7 @@ export default {
{{ __('Toggle discussion') }}
</button>
</div>
- <a v-if="hasAuthor" :href="author.path">
+ <a v-if="hasAuthor" v-once :href="author.path">
<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 29740ddf6ae..f4991a41325 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,9 +1,12 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
+import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -20,14 +23,12 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
icon,
noteableNote,
- diffWithNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
@@ -37,9 +38,10 @@ export default {
placeholderNote,
placeholderSystemNote,
systemNote,
+ TimelineEntryItem,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
mixins: [autosave, noteable, resolvable, discussionNavigation],
props: {
@@ -64,43 +66,24 @@ export default {
},
},
data() {
+ const { diff_discussion: isDiffDiscussion, resolved } = this.discussion;
+
return {
isReplying: false,
isResolving: false,
resolveAsThread: true,
- isRepliesToggledByUser: false,
+ isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved),
};
},
computed: {
...mapGetters([
'getNoteableData',
- 'discussionCount',
- 'resolvedDiscussionCount',
- 'allDiscussions',
- 'unresolvedDiscussionsIdsByDiff',
- 'unresolvedDiscussionsIdsByDate',
- 'unresolvedDiscussions',
- 'unresolvedDiscussionsIdsOrdered',
'nextUnresolvedDiscussionId',
- 'isLastUnresolvedDiscussion',
+ 'unresolvedDiscussionsCount',
+ 'hasUnresolvedDiscussions',
]),
- transformedDiscussion() {
- return {
- ...this.discussion.notes[0],
- truncated_diff_lines: this.discussion.truncated_diff_lines || [],
- truncated_diff_lines_path: this.discussion.truncated_diff_lines_path,
- diff_file: this.discussion.diff_file,
- diff_discussion: this.discussion.diff_discussion,
- active: this.discussion.active,
- discussion_path: this.discussion.discussion_path,
- resolved: this.discussion.resolved,
- resolved_by: this.discussion.resolved_by,
- resolved_by_push: this.discussion.resolved_by_push,
- resolved_at: this.discussion.resolved_at,
- };
- },
author() {
- return this.transformedDiscussion.author;
+ return this.initialDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
@@ -136,29 +119,13 @@ export default {
return null;
},
resolvedText() {
- return this.transformedDiscussion.resolved_by_push ? 'Automatically resolved' : 'Resolved';
- },
- hasMultipleUnresolvedDiscussions() {
- return this.unresolvedDiscussions.length > 1;
- },
- showJumpToNextDiscussion() {
- return (
- this.hasMultipleUnresolvedDiscussions &&
- !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
- );
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
},
shouldRenderDiffs() {
- return (
- this.transformedDiscussion.diff_discussion &&
- this.transformedDiscussion.diff_file &&
- this.renderDiffFile
- );
+ return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
- return !this.shouldRenderDiffs && !this.transformedDiscussion.diff_discussion;
- },
- shouldRenderHeader() {
- return this.shouldRenderDiffs;
+ return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
@@ -170,9 +137,6 @@ export default {
return {};
},
- wrapperClass() {
- return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
- },
componentClassName() {
if (this.shouldRenderDiffs) {
if (!this.lastUpdatedAt && !this.discussion.resolved) {
@@ -183,19 +147,40 @@ export default {
return '';
},
shouldShowDiscussions() {
- const isExpanded = this.discussion.expanded;
- const { resolved } = this.transformedDiscussion;
- const isResolvedNonDiffDiscussion = !this.transformedDiscussion.diff_discussion && resolved;
+ const { expanded, resolved } = this.discussion;
+ const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved;
- return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
},
- isRepliesCollapsed() {
- const { discussion, isRepliesToggledByUser } = this;
- const { resolved, notes } = discussion;
- const hasReplies = notes.length > 1;
+ 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 text = s__('MergeRequests|started a discussion');
- return (
- (!discussion.diff_discussion && resolved && hasReplies && !isRepliesToggledByUser) || false
+ if (this.discussion.for_commit) {
+ text = s__(
+ 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
+ );
+ } else if (this.discussion.diff_discussion) {
+ if (this.discussion.active) {
+ text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}');
+ } else {
+ text = s__(
+ 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
+ );
+ }
+ }
+
+ return sprintf(
+ text,
+ {
+ commitId,
+ linkStart,
+ linkEnd,
+ },
+ false,
);
},
},
@@ -204,7 +189,7 @@ export default {
if (this.isReplying) {
this.$nextTick(() => {
// Pass an extra key to separate reply and note edit forms
- this.initAutoSave(this.transformedDiscussion, ['Reply']);
+ this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']);
});
} else {
this.disposeAutoSave();
@@ -242,7 +227,7 @@ export default {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
toggleReplies() {
- this.isRepliesToggledByUser = !this.isRepliesToggledByUser;
+ this.isRepliesCollapsed = !this.isRepliesCollapsed;
},
showReplyForm() {
this.isReplying = true;
@@ -311,181 +296,156 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry" :class="componentClassName">
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <div
- :data-discussion-id="transformedDiscussion.discussion_id"
- class="discussion js-discussion-container"
- >
- <div v-if="shouldRenderHeader" class="discussion-header note-wrapper">
- <div class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <note-header
- :author="author"
- :created-at="transformedDiscussion.created_at"
- :note-id="transformedDiscussion.id"
- :include-toggle="true"
- :expanded="discussion.expanded"
- @toggleHandler="toggleDiscussionHandler"
- >
- <template v-if="transformedDiscussion.diff_discussion">
- started a discussion on
- <a :href="transformedDiscussion.discussion_path">
- <template v-if="transformedDiscussion.active">
- the diff
- </template>
- <template v-else>
- an old version of the diff
- </template>
- </a>
- </template>
- <template v-else-if="discussion.for_commit">
- started a discussion on commit
- <a :href="discussion.discussion_path"> {{ truncateSha(discussion.commit_id) }} </a>
- </template>
- <template v-else>
- started a discussion
- </template>
- </note-header>
- <note-edited-text
- v-if="transformedDiscussion.resolved"
- :edited-at="transformedDiscussion.resolved_at"
- :edited-by="transformedDiscussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline"
- />
- <note-edited-text
- v-else-if="lastUpdatedAt"
- :edited-at="lastUpdatedAt"
- :edited-by="lastUpdatedBy"
- action-text="Last updated"
- class-name="discussion-headline-light js-discussion-headline"
+ <timeline-entry-item class="note note-discussion" :class="componentClassName">
+ <div class="timeline-content">
+ <div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
+ <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
/>
</div>
- <div v-if="shouldShowDiscussions" class="discussion-body">
- <component :is="wrapperComponent" v-bind="wrapperComponentProps" :class="wrapperClass">
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(initialDiscussion)"
- :note="componentData(initialDiscussion)"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot slot="avatar-badge" name="avatar-badge"> </slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="isRepliesCollapsed"
- :replies="replies"
- @toggle="toggleReplies"
- />
- <template v-if="!isRepliesCollapsed">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
+ <note-header
+ :author="author"
+ :created-at="initialDiscussion.created_at"
+ :note-id="initialDiscussion.id"
+ :include-toggle="true"
+ :expanded="discussion.expanded"
+ @toggleHandler="toggleDiscussionHandler"
+ >
+ <span v-html="actionText"></span>
+ </note-header>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ <note-edited-text
+ v-else-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ action-text="Last updated"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
+ <div v-if="shouldShowDiscussions" class="discussion-body">
+ <component
+ :is="wrapperComponent"
+ v-bind="wrapperComponentProps"
+ class="card discussion-wrapper"
+ >
+ <div class="discussion-notes">
+ <ul class="notes">
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(initialDiscussion)"
+ :note="componentData(initialDiscussion)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
<component
:is="componentName(note)"
- v-for="(note, index) in discussion.notes"
+ v-for="note in replies"
:key="note.id"
:note="componentData(note)"
@handleDeleteNote="deleteNoteHandler"
- >
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"> </slot>
- </component>
+ />
</template>
- </ul>
- <div
- v-if="!isRepliesCollapsed"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <template v-if="!isReplying && canReply">
- <div class="discussion-with-resolve-btn">
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <div
+ v-if="!isRepliesCollapsed"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <template v-if="!isReplying && canReply">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm"
+ class="btn btn-default mr-sm-2"
+ @click="resolveHandler();"
>
- Reply...
+ <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
+ {{ resolveButtonTitle }}
</button>
- <div v-if="discussion.resolvable">
+ </div>
+ <div
+ v-if="discussion.resolvable"
+ class="btn-group discussion-actions ml-sm-2"
+ role="group"
+ >
+ <div v-if="!discussionResolved" class="btn-group" role="group">
+ <a
+ v-gl-tooltip
+ :href="discussion.resolve_with_issue_path"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
+ class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
+ >
+ <icon name="issue-new" />
+ </a>
+ </div>
+ <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
<button
- type="button"
- class="btn btn-default mr-sm-2"
- @click="resolveHandler();"
+ v-gl-tooltip
+ class="btn btn-default discussion-next-btn"
+ title="Jump to next unresolved discussion"
+ @click="jumpToNextDiscussion"
>
- <i
- v-if="isResolving"
- aria-hidden="true"
- class="fa fa-spinner fa-spin"
- ></i>
- {{ resolveButtonTitle }}
+ <icon name="comment-next" />
</button>
</div>
- <div
- v-if="discussion.resolvable"
- class="btn-group discussion-actions ml-sm-2"
- role="group"
- >
- <div v-if="!discussionResolved" class="btn-group" role="group">
- <a
- v-tooltip
- :href="discussion.resolve_with_issue_path"
- :title="s__('MergeRequests|Resolve this discussion in a new issue')"
- class="new-issue-for-discussion btn
- btn-default discussion-create-issue-btn"
- data-container="body"
- >
- <icon name="issue-new" />
- </a>
- </div>
- <div v-if="showJumpToNextDiscussion" class="btn-group" role="group">
- <button
- v-tooltip
- class="btn btn-default discussion-next-btn"
- title="Jump to next unresolved discussion"
- data-container="body"
- @click="jumpToNextDiscussion"
- >
- <icon name="comment-next" />
- </button>
- </div>
- </div>
</div>
- </template>
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- save-button-title="Comment"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
+ </div>
+ </template>
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ save-button-title="Comment"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!canReply" />
</div>
- </component>
- </div>
+ </div>
+ </component>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index c2e49f8b23f..a17be51353e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -18,6 +19,7 @@ export default {
noteHeader,
noteActions,
noteBody,
+ TimelineEntryItem,
},
mixins: [noteable, resolvable],
props: {
@@ -169,61 +171,60 @@ export default {
</script>
<template>
- <li
+ <timeline-entry-item
:id="noteAnchorId"
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry note-wrapper"
+ class="note note-wrapper"
>
- <div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- >
- <slot slot="avatar-badge" name="avatar-badge"> </slot>
- </user-avatar-link>
- </div>
- <div class="timeline-content">
- <div class="note-header">
- <note-header
- :author="author"
- :created-at="note.created_at"
- :note-id="note.id"
- action-text="commented"
- />
- <note-actions
- :author-id="author.id"
- :note-id="note.id"
- :note-url="note.noteable_note_url"
- :access-level="note.human_access"
- :can-edit="note.current_user.can_edit"
- :can-award-emoji="note.current_user.can_award_emoji"
- :can-delete="note.current_user.can_edit"
- :can-report-as-abuse="canReportAsAbuse"
- :can-resolve="note.current_user.can_resolve"
- :report-abuse-path="note.report_abuse_path"
- :resolvable="note.resolvable"
- :is-resolved="note.resolved"
- :is-resolving="isResolving"
- :resolved-by="note.resolved_by"
- @handleEdit="editHandler"
- @handleDelete="deleteHandler"
- @handleResolve="resolveHandler"
- />
- </div>
- <note-body
- ref="noteBody"
- :note="note"
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"> </slot>
+ </user-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ v-once
+ :author="author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ action-text="commented"
+ />
+ <note-actions
+ :author-id="author.id"
+ :note-id="note.id"
+ :note-url="note.noteable_note_url"
+ :access-level="note.human_access"
:can-edit="note.current_user.can_edit"
- :is-editing="isEditing"
- @handleFormUpdate="formUpdateHandler"
- @cancelForm="formCancelHandler"
+ :can-award-emoji="note.current_user.can_award_emoji"
+ :can-delete="note.current_user.can_edit"
+ :can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
+ :report-abuse-path="note.report_abuse_path"
+ :resolvable="note.resolvable"
+ :is-resolved="note.resolved"
+ :is-resolving="isResolving"
+ :resolved-by="note.resolved_by"
+ @handleEdit="editHandler"
+ @handleDelete="deleteHandler"
+ @handleResolve="resolveHandler"
/>
</div>
+ <note-body
+ ref="noteBody"
+ :note="note"
+ :can-edit="note.current_user.can_edit"
+ :is-editing="isEditing"
+ @handleFormUpdate="formUpdateHandler"
+ @cancelForm="formCancelHandler"
+ />
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 79ece036e69..6e6efb04753 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -22,6 +22,7 @@ export default {
commentForm,
placeholderNote,
placeholderSystemNote,
+ skeletonLoadingContainer,
},
props: {
noteableData: {
@@ -59,7 +60,6 @@ export default {
'isNotesFetched',
'discussions',
'getNotesDataByProp',
- 'discussionCount',
'isLoading',
'commentsDisabled',
]),
@@ -109,39 +109,22 @@ export default {
this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
},
methods: {
- ...mapActions({
- setLoadingState: 'setLoadingState',
- fetchDiscussions: 'fetchDiscussions',
- poll: 'poll',
- actionToggleAward: 'toggleAward',
- scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
- setNotesData: 'setNotesData',
- setNoteableData: 'setNoteableData',
- setUserData: 'setUserData',
- setLastFetchedAt: 'setLastFetchedAt',
- setTargetNoteHash: 'setTargetNoteHash',
- toggleDiscussion: 'toggleDiscussion',
- setNotesFetchedState: 'setNotesFetchedState',
- startTaskList: 'startTaskList',
- }),
- getComponentName(discussion) {
- if (discussion.isSkeletonNote) {
- return skeletonLoadingContainer;
- }
- if (discussion.isPlaceholderNote) {
- if (discussion.placeholderType === constants.SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
- return placeholderNote;
- } else if (discussion.individual_note) {
- return discussion.notes[0].system ? systemNote : noteableNote;
- }
-
- return noteableDiscussion;
- },
- getComponentData(discussion) {
- return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
- },
+ ...mapActions([
+ 'setLoadingState',
+ 'fetchDiscussions',
+ 'poll',
+ 'toggleAward',
+ 'scrollToNoteIfNeeded',
+ 'setNotesData',
+ 'setNoteableData',
+ 'setUserData',
+ 'setLastFetchedAt',
+ 'setTargetNoteHash',
+ 'toggleDiscussion',
+ 'setNotesFetchedState',
+ 'expandDiscussion',
+ 'startTaskList',
+ ]),
fetchNotes() {
if (this.isFetching) return null;
@@ -181,31 +164,46 @@ export default {
const noteId = hash && hash.replace(/^note_/, '');
if (noteId) {
- this.discussions.forEach(discussion => {
- if (discussion.notes) {
- discussion.notes.forEach(note => {
- if (`${note.id}` === `${noteId}`) {
- // FIXME: this modifies the store state without using a mutation/action
- Object.assign(discussion, { expanded: true });
- }
- });
- }
- });
+ const discussion = this.discussions.find(d => d.notes.some(({ id }) => id === noteId));
+
+ if (discussion) {
+ this.expandDiscussion({ discussionId: discussion.id });
+ }
}
},
},
+ systemNote: constants.SYSTEM_NOTE,
};
</script>
<template>
<div v-show="shouldShow" id="notes">
<ul id="notes-list" class="notes main-notes-list timeline">
- <component
- :is="getComponentName(discussion)"
- v-for="discussion in allDiscussions"
- :key="discussion.id"
- v-bind="getComponentData(discussion)"
- />
+ <template v-for="discussion in allDiscussions">
+ <skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
+ <template v-else-if="discussion.isPlaceholderNote">
+ <placeholder-system-note
+ v-if="discussion.placeholderType === $options.systemNote"
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ />
+ <placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ </template>
+ <template v-else-if="discussion.individual_note">
+ <system-note
+ v-if="discussion.notes[0].system"
+ :key="discussion.id"
+ :note="discussion.notes[0]"
+ />
+ <noteable-note v-else :key="discussion.id" :note="discussion.notes[0]" />
+ </template>
+ <noteable-discussion
+ v-else
+ :key="discussion.id"
+ :discussion="discussion"
+ :render-diff-file="true"
+ />
+ </template>
</ul>
<comment-form
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index cd8394e0619..8edf3d088bb 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -36,7 +36,7 @@ export default {
const discussion = this.resolveAsThread;
const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
- this.toggleResolveNote({ endpoint, isResolved, discussion })
+ return this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
this.isResolving = false;
})
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 5b2f0540020..b4befdd6e4a 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -11,7 +11,7 @@ import * as constants from '../constants';
import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
-import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
+import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale';
@@ -39,12 +39,13 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchDiscussions = ({ commit }, { path, filter }) =>
+export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
service
.fetchDiscussions(path, filter)
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ dispatch('updateResolvableDiscussonsCounts');
});
export const updateDiscussion = ({ commit, state }, discussion) => {
@@ -53,11 +54,18 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
return utils.findNoteObjectById(state.discussions, discussion.id);
};
-export const deleteNote = ({ commit, dispatch }, note) =>
+export const deleteNote = ({ commit, dispatch, state }, note) =>
service.deleteNote(note.path).then(() => {
+ const discussion = state.discussions.find(({ id }) => id === note.discussion_id);
+
commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget');
+ dispatch('updateResolvableDiscussonsCounts');
+
+ if (isInMRPage()) {
+ dispatch('diffs/removeDiscussionsFromDiff', discussion);
+ }
});
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
@@ -89,6 +97,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
+ dispatch('updateResolvableDiscussonsCounts');
}
return res;
});
@@ -104,6 +113,8 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res);
+ dispatch('updateResolvableDiscussonsCounts');
+
dispatch('updateMergeRequestWidget');
});
@@ -385,5 +396,8 @@ export const startTaskList = ({ dispatch }) =>
}),
);
+export const updateResolvableDiscussonsCounts = ({ commit }) =>
+ commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 980d79605d7..2ed8aac059a 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -53,30 +53,15 @@ export const getCurrentUserLastNote = state =>
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
-export const discussionCount = state => {
- const filteredDiscussions = state.discussions.filter(n => !n.individual_note && n.resolvable);
-
- return filteredDiscussions.length;
-};
-
-export const unresolvedDiscussions = (state, getters) => {
- const resolvedMap = getters.resolvedDiscussionsById;
-
- return state.discussions.filter(n => !n.individual_note && !resolvedMap[n.id]);
-};
-
-export const allDiscussions = (state, getters) => {
- const resolved = getters.resolvedDiscussionsById;
- const unresolved = getters.unresolvedDiscussions;
-
- return Object.values(resolved).concat(unresolved);
-};
+export const unresolvedDiscussionsCount = state => state.unresolvedDiscussionsCount;
+export const resolvableDiscussionsCount = state => state.resolvableDiscussionsCount;
+export const hasUnresolvedDiscussions = state => state.hasUnresolvedDiscussions;
export const isDiscussionResolved = (state, getters) => discussionId =>
getters.resolvedDiscussionsById[discussionId] !== undefined;
-export const allResolvableDiscussions = (state, getters) =>
- getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
+export const allResolvableDiscussions = state =>
+ state.discussions.filter(d => !d.individual_note && d.resolvable);
export const resolvedDiscussionsById = state => {
const map = {};
@@ -147,15 +132,12 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
-export const discussionTabCounter = state => {
- let all = [];
-
- state.discussions.forEach(discussion => {
- all = all.concat(discussion.notes.filter(note => !note.system && !note.placeholder));
- });
-
- return all.length;
-};
+export const discussionTabCounter = state =>
+ state.discussions.reduce(
+ (acc, discussion) =>
+ acc + discussion.notes.filter(note => !note.system && !note.placeholder).length,
+ 0,
+ );
// Returns the list of discussion IDs ordered according to given parameter
// @param {Boolean} diffOrder - is ordered by diff?
@@ -182,8 +164,10 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif
export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
const currentIndex = idsOrdered.indexOf(discussionId);
+ const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2);
- return idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0];
+ // Get the first ID if there is none after the currentIndex
+ return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
};
// @param {Boolean} diffOrder - is ordered by diff?
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 8aea269ea7d..b5fe8bdb1d3 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -22,6 +22,9 @@ export default () => ({
current_user: {},
},
commentsDisabled: false,
+ resolvableDiscussionsCount: 0,
+ unresolvedDiscussionsCount: 0,
+ hasUnresolvedDiscussions: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index dfbf3b7b34b..9c68ab67a8c 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -21,6 +21,7 @@ export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
+export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index f6054e0be87..bea396e5bb6 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -24,6 +24,7 @@ export default {
noteData.resolved = false;
noteData.resolve_path = note.resolve_path;
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
+ noteData.diff_discussion = false;
}
state.discussions.push(noteData);
@@ -97,33 +98,36 @@ export default {
},
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
- const discussions = [];
+ const discussions = discussionsData.reduce((acc, d) => {
+ const discussion = { ...d };
+ const diffData = {};
- discussionsData.forEach(discussion => {
if (discussion.diff_file) {
- Object.assign(discussion, {
- file_hash: discussion.diff_file.file_hash,
- truncated_diff_lines: discussion.truncated_diff_lines || [],
- });
+ diffData.file_hash = discussion.diff_file.file_hash;
+ diffData.truncated_diff_lines = discussion.truncated_diff_lines || [];
}
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach(n => {
- discussions.push({
+ acc.push({
...discussion,
+ ...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note
});
});
} else {
const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
- discussions.push({
+ acc.push({
...discussion,
+ ...diffData,
expanded: oldNote ? oldNote.expanded : discussion.expanded,
});
}
- });
+
+ return acc;
+ }, []);
Object.assign(state, { discussions });
},
@@ -174,9 +178,11 @@ export default {
}
},
- [types.TOGGLE_DISCUSSION](state, { discussionId }) {
+ [types.TOGGLE_DISCUSSION](state, { discussionId, forceExpanded = null }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
- Object.assign(discussion, { expanded: !discussion.expanded });
+ Object.assign(discussion, {
+ expanded: forceExpanded === null ? !discussion.expanded : forceExpanded,
+ });
},
[types.UPDATE_NOTE](state, note) {
@@ -195,7 +201,9 @@ export default {
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
if (note.diff_file) {
- Object.assign(note, { file_hash: note.diff_file.file_hash });
+ Object.assign(note, {
+ file_hash: note.diff_file.file_hash,
+ });
}
Object.assign(selectedDiscussion, { ...note });
},
@@ -229,4 +237,16 @@ export default {
[types.DISABLE_COMMENTS](state, value) {
state.commentsDisabled = value;
},
+ [types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS](state) {
+ state.resolvableDiscussionsCount = state.discussions.filter(
+ discussion => !discussion.individual_note && discussion.resolvable,
+ ).length;
+ state.unresolvedDiscussionsCount = state.discussions.filter(
+ discussion =>
+ !discussion.individual_note &&
+ discussion.resolvable &&
+ discussion.notes.some(note => !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/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/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 32b55575f95..01ef445c901 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
+import groupsSelect from '~/groups_select';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
@@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
);
mountBadgeSettings(GROUP_BADGE);
+ // Initialize Subgroups selector
+ groupsSelect();
+
projectSelect();
});
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/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/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index d3e8dbf4000..9b58d42b47d 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,5 +1,4 @@
import bp from '../../../breakpoints';
-import { slugify } from '../../../lib/utils/text_utility';
import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
@@ -26,7 +25,8 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = slugify(slugInput.value);
+
+ const slug = slugInput.value;
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/javascripts/pages/root/index.js b/app/assets/javascripts/pages/root/index.js
deleted file mode 100644
index 09f8185d3b5..00000000000
--- a/app/assets/javascripts/pages/root/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// if the "projects dashboard" is a user's default dashboard, when they visit the
-// instance root index, the dashboard will be served by the root controller instead
-// of a dashboard controller. The root index redirects for all other default dashboards.
-
-import '../dashboard/projects/index';
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/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 6f008528db4..59cebaba717 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -18,23 +18,19 @@ export default {
required: true,
},
},
-
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
},
-
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
-
isFirstColumn(index) {
return index === 0;
},
-
stageConnectorClass(index, stage) {
let className;
@@ -48,7 +44,6 @@ export default {
return className;
},
-
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 782494f72e4..cf9db89e32b 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -84,10 +84,6 @@ export default {
return textBuilder.join(' ');
},
-
- tooltipBoundary() {
- return this.dropdownLength < 5 ? 'viewport' : null;
- },
/**
* Verifies if the provided job has an action path
*
@@ -108,7 +104,7 @@ export default {
<div class="ci-job-component">
<gl-link
v-if="status.has_details"
- v-gl-tooltip="{ boundary: tooltipBoundary }"
+ v-gl-tooltip
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index e5924d3a77e..30a5bbf92ce 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -65,7 +65,7 @@ export default {
v-if="pipeline.flags.latest"
v-gl-tooltip
class="js-pipeline-url-latest badge badge-success"
- title="Latest pipeline for this branch"
+ title="__('Latest pipeline for this branch')"
>
latest
</span>
@@ -97,6 +97,14 @@ export default {
<span v-if="pipeline.flags.stuck" class="js-pipeline-url-stuck badge badge-warning">
stuck
</span>
+ <span
+ v-if="pipeline.flags.merge_request"
+ v-gl-tooltip
+ title="__('This pipeline is run in a merge request context')"
+ class="js-pipeline-url-mergerequest badge badge-info"
+ >
+ merge request
+ </span>
</div>
</div>
</template>
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 007b83e1927..9af5d5b23cb 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -23,11 +23,11 @@ export default class Star {
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$startIcon.remove();
- $this.prepend(spriteIcon('star-o'));
+ $this.prepend(spriteIcon('star-o', 'icon'));
} else {
$starSpan.addClass('starred').text(__('Unstar'));
$startIcon.remove();
- $this.prepend(spriteIcon('star'));
+ $this.prepend(spriteIcon('star', 'icon'));
}
})
.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/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 950347d8863..2f2a37347af 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div class="mr-widget-heading deploy-heading append-bottom-default">
+ <div class="deploy-heading">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
new file mode 100644
index 00000000000..5967ca026e5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
@@ -0,0 +1,6 @@
+<template>
+ <div class="mr-widget-heading">
+ <div class="mr-widget-content"><slot name="default"></slot></div>
+ <slot name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 6f422ea3f27..3b9fc2661ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetHeader',
@@ -13,6 +14,7 @@ export default {
Icon,
clipboardButton,
TooltipOnTruncate,
+ MrWidgetIcon,
},
directives: {
tooltip,
@@ -76,7 +78,7 @@ export default {
</script>
<template>
<div class="mr-source-target append-bottom-default">
- <div class="git-merge-icon-container append-right-default"><icon name="git-merge" /></div>
+ <mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex">
<div class="normal">
<strong>
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
new file mode 100644
index 00000000000..e3adc7f7af5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -0,0 +1,17 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: { Icon },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="circle-icon-container append-right-default"><icon :name="name" /></div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 71571ba9cab..f11cf21b0ca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -79,67 +79,65 @@ export default {
</script>
<template>
- <div v-if="hasPipeline || hasCIError" class="mr-widget-heading append-bottom-default">
- <div class="ci-widget media">
- <template v-if="hasCIError">
- <div
- class="add-border ci-status-icon ci-status-icon-failed ci-error
- js-ci-error append-right-default"
- >
- <icon :size="32" name="status_failed_borderless" />
- </div>
- <div class="media-body" v-html="errorText"></div>
- </template>
- <template v-else-if="hasPipeline">
- <a :href="status.details_path" class="align-self-start append-right-default">
- <ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
- </a>
- <div class="ci-widget-container d-flex">
- <div class="ci-widget-content">
- <div class="media-body">
- <div class="font-weight-bold">
- Pipeline
- <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
- >#{{ pipeline.id }}</a
- >
+ <div v-if="hasPipeline || hasCIError" class="ci-widget media">
+ <template v-if="hasCIError">
+ <div
+ class="add-border ci-status-icon ci-status-icon-failed ci-error
+ js-ci-error append-right-default"
+ >
+ <icon :size="32" name="status_failed_borderless" />
+ </div>
+ <div class="media-body" v-html="errorText"></div>
+ </template>
+ <template v-else-if="hasPipeline">
+ <a :href="status.details_path" class="align-self-start append-right-default">
+ <ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
+ </a>
+ <div class="ci-widget-container d-flex">
+ <div class="ci-widget-content">
+ <div class="media-body">
+ <div class="font-weight-bold">
+ Pipeline
+ <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</a
+ >
- {{ pipeline.details.status.label }}
+ {{ pipeline.details.status.label }}
- <template v-if="hasCommitInfo">
- for
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link font-weight-normal"
- >
- {{ pipeline.commit.short_id }}</a
- >
- on
- <tooltip-on-truncate
- :title="sourceBranch"
- truncate-target="child"
- class="label-branch label-truncate"
- v-html="sourceBranchLink"
- />
- </template>
- </div>
- <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
+ <template v-if="hasCommitInfo">
+ for
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link font-weight-normal"
+ >
+ {{ pipeline.commit.short_id }}</a
+ >
+ on
+ <tooltip-on-truncate
+ :title="sourceBranch"
+ truncate-target="child"
+ class="label-branch label-truncate"
+ v-html="sourceBranchLink"
+ />
+ </template>
</div>
+ <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
</div>
- <div>
- <span class="mr-widget-pipeline-graph">
- <span v-if="hasStages" class="stage-cell">
- <div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
- >
- <pipeline-stage :stage="stage" />
- </div>
- </span>
+ </div>
+ <div>
+ <span class="mr-widget-pipeline-graph">
+ <span v-if="hasStages" class="stage-cell">
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
</span>
- </div>
+ </span>
</div>
- </template>
- </div>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
new file mode 100644
index 00000000000..5f5fe67b3c1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -0,0 +1,74 @@
+<script>
+import Deployment from './deployment.vue';
+import MrWidgetContainer from './mr_widget_container.vue';
+import MrWidgetPipeline from './mr_widget_pipeline.vue';
+
+/**
+ * Renders the pipeline and related deployments from the store.
+ *
+ * | Props | Description
+ * |---------------|-------------
+ * | `mr` | This is the mr_widget store
+ * | `isPostMerge` | If true, show the "post merge" pipeline and deployments
+ */
+export default {
+ name: 'MrWidgetPipelineContainer',
+ components: {
+ Deployment,
+ MrWidgetContainer,
+ MrWidgetPipeline,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ isPostMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ pipeline() {
+ return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
+ },
+ branch() {
+ return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
+ },
+ branchLink() {
+ return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranchLink;
+ },
+ deployments() {
+ return this.isPostMerge ? this.mr.postMergeDeployments : this.mr.deployments;
+ },
+ deploymentClass() {
+ return this.isPostMerge ? 'js-post-deployment' : 'js-pre-deployment';
+ },
+ hasDeploymentMetrics() {
+ return this.isPostMerge;
+ },
+ },
+};
+</script>
+<template>
+ <mr-widget-container>
+ <mr-widget-pipeline
+ :pipeline="pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ :source-branch="branch"
+ :source-branch-link="branchLink"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ />
+ <div v-if="deployments.length" slot="footer" class="mr-widget-extension">
+ <deployment
+ v-for="deployment in deployments"
+ :key="deployment.id"
+ :class="deploymentClass"
+ :deployment="deployment"
+ :show-metrics="hasDeploymentMetrics"
+ />
+ </div>
+ </mr-widget-container>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index a269c0a4e87..3c3e3efcc36 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
-import WidgetPipeline from './components/mr_widget_pipeline.vue';
+import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MergedState from './components/states/mr_widget_merged.vue';
@@ -44,7 +44,7 @@ export default {
components: {
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
- 'mr-widget-pipeline': WidgetPipeline,
+ MrWidgetPipelineContainer,
Deployment,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
@@ -296,23 +296,12 @@ export default {
<template>
<div class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
- <mr-widget-pipeline
+ <mr-widget-pipeline-container
v-if="shouldRenderPipelines"
- :pipeline="mr.pipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- :source-branch="mr.sourceBranch"
- :source-branch-link="mr.sourceBranchLink"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ class="mr-widget-workflow"
+ :mr="mr"
/>
- <deployment
- v-for="deployment in mr.deployments"
- :key="`pre-merge-deploy-${deployment.id}`"
- class="js-pre-merge-deploy"
- :deployment="deployment"
- :show-metrics="false"
- />
- <div class="mr-section-container">
+ <div class="mr-section-container mr-widget-workflow">
<grouped-test-reports-app
v-if="mr.testResultsPath"
class="js-reports-container"
@@ -336,24 +325,11 @@ export default {
</div>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
</div>
-
- <template v-if="shouldRenderMergedPipeline">
- <mr-widget-pipeline
- class="js-post-merge-pipeline prepend-top-default"
- :pipeline="mr.mergePipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- :source-branch="mr.targetBranch"
- :source-branch-link="mr.targetBranch"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
- />
- <deployment
- v-for="postMergeDeployment in mr.postMergeDeployments"
- :key="`post-merge-deploy-${postMergeDeployment.id}`"
- :deployment="postMergeDeployment"
- :show-metrics="true"
- class="js-post-deployment"
- />
- </template>
+ <mr-widget-pipeline-container
+ v-if="shouldRenderMergedPipeline"
+ class="js-post-merge-pipeline mr-widget-workflow"
+ :mr="mr"
+ :is-post-merge="true"
+ />
</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/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index b1faebf409b..8d3a3009c55 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -17,12 +17,14 @@
* />
*/
import { mapGetters } from 'vuex';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
components: {
userAvatarLink,
+ TimelineEntryItem,
},
props: {
note: {
@@ -37,30 +39,28 @@ export default {
</script>
<template>
- <li class="note being-posted fade-in-half timeline-entry">
- <div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- :link-href="getUserData.path"
- :img-src="getUserData.avatar_url"
- :img-size="40"
- />
- </div>
- <div :class="{ discussion: !note.individual_note }" class="timeline-content">
- <div class="note-header">
- <div class="note-header-info">
- <a :href="getUserData.path">
- <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
- <span class="note-headline-light">@{{ getUserData.username }}</span>
- </a>
- </div>
+ <timeline-entry-item class="note being-posted fade-in-half">
+ <div class="timeline-icon">
+ <user-avatar-link
+ :link-href="getUserData.path"
+ :img-src="getUserData.avatar_url"
+ :img-size="40"
+ />
+ </div>
+ <div :class="{ discussion: !note.individual_note }" class="timeline-content">
+ <div class="note-header">
+ <div class="note-header-info">
+ <a :href="getUserData.path">
+ <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
+ <span class="note-headline-light">@{{ getUserData.username }}</span>
+ </a>
</div>
- <div class="note-body">
- <div class="note-text">
- <p>{{ note.body }}</p>
- </div>
+ </div>
+ <div class="note-body">
+ <div class="note-text">
+ <p>{{ note.body }}</p>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
index 674f923478d..7689425eb52 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
@@ -1,4 +1,6 @@
<script>
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
/**
* Common component to render a placeholder system note.
*
@@ -9,6 +11,9 @@
*/
export default {
name: 'PlaceholderSystemNote',
+ components: {
+ TimelineEntryItem,
+ },
props: {
note: {
type: Object,
@@ -19,11 +24,9 @@ export default {
</script>
<template>
- <li class="note system-note timeline-entry being-posted fade-in-half">
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <em>{{ note.body }}</em>
- </div>
+ <timeline-entry-item class="note system-note being-posted fade-in-half">
+ <div class="timeline-content">
+ <em>{{ note.body }}</em>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index c6cf4661222..e61d1fd2031 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,22 +1,22 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'SkeletonNote',
components: {
GlSkeletonLoading,
+ TimelineEntryItem,
},
};
</script>
<template>
- <li class="timeline-entry note note-wrapper">
- <div class="timeline-entry-inner">
- <div class="timeline-icon"></div>
- <div class="timeline-content">
- <div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loading /></div>
- </div>
+ <timeline-entry-item class="note note-wrapper">
+ <div class="timeline-icon"></div>
+ <div class="timeline-content">
+ <div class="note-header"></div>
+ <div class="note-body"><gl-skeleton-loading /></div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index fb86262d0b4..31df26f7b05 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -20,6 +20,7 @@ import $ from 'jquery';
import { mapGetters } from 'vuex';
import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -29,6 +30,7 @@ export default {
components: {
Icon,
noteHeader,
+ TimelineEntryItem,
},
props: {
note: {
@@ -73,36 +75,34 @@ export default {
</script>
<template>
- <li
+ <timeline-entry-item
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry note-wrapper"
+ class="note system-note note-wrapper"
>
- <div class="timeline-entry-inner">
- <div class="timeline-icon" v-html="iconHtml"></div>
- <div class="timeline-content">
- <div class="note-header">
- <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-html="actionTextHtml"></span>
- </note-header>
- </div>
- <div class="note-body">
- <div
- :class="{
- 'system-note-commit-list': hasMoreCommits,
- 'hide-shade': expanded,
- }"
- class="note-text"
- v-html="note.note_html"
- ></div>
- <div v-if="hasMoreCommits" class="flex-list">
- <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
- <icon :name="toggleIcon" :size="8" class="append-right-5" />
- <span>Toggle commit list</span>
- </div>
+ <div class="timeline-icon" v-html="iconHtml"></div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
+ <span v-html="actionTextHtml"></span>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ :class="{
+ 'system-note-commit-list': hasMoreCommits,
+ 'hide-shade': expanded,
+ }"
+ class="note-text"
+ v-html="note.note_html"
+ ></div>
+ <div v-if="hasMoreCommits" class="flex-list">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
+ <icon :name="toggleIcon" :size="8" class="append-right-5" />
+ <span>Toggle commit list</span>
</div>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue
new file mode 100644
index 00000000000..06974a12aed
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue
@@ -0,0 +1,11 @@
+<script>
+export default {
+ name: 'TimelineEntryItem',
+};
+</script>
+
+<template>
+ <li class="timeline-entry">
+ <div class="timeline-entry-inner"><slot></slot></div>
+ </li>
+</template>
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/stylesheets/bootstrap.scss b/app/assets/stylesheets/bootstrap.scss
index a040c2f8c20..4a09da3d580 100644
--- a/app/assets/stylesheets/bootstrap.scss
+++ b/app/assets/stylesheets/bootstrap.scss
@@ -1,5 +1,5 @@
/*
- * Includes specific styles from the bootstrap4 foler in node_modules
+ * Includes specific styles from the bootstrap4 folder in node_modules
*/
@import "../../../node_modules/bootstrap/scss/functions";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 1e00aa4ff7e..62024b8c555 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -336,3 +336,12 @@ input[type=color].form-control {
.input-group-btn:last-child {
@extend .input-group-append;
}
+
+/*
+ Bootstrap 4.1.2 introduced a new default vertical alignment which breaks our icons,
+ so we need to reset the vertical alignment to the default value. See:
+ - https://gitlab.com/gitlab-org/gitlab-ce/issues/51362
+ */
+svg {
+ vertical-align: baseline;
+}
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..054c75912ea 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; }
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 219fd99b097..e36f99ac577 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-small-font-size;
+ line-height: $gl-btn-small-line-height;
}
&.btn-success,
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index bdd7f09d926..0d8e4afa76f 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -33,7 +33,11 @@
.bs-callout-warning {
background-color: $orange-100;
border-color: $orange-200;
- color: $orange-700;
+ color: $orange-900;
+
+ a {
+ color: $orange-900;
+ }
}
.bs-callout-info {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 626c8f92d1d..f2f3a45ca09 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -386,3 +386,4 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; }
+.min-height-0 { min-height: 0; }
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..f3c44f32d6f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -294,10 +294,10 @@
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 +306,7 @@
height: 1px;
margin-top: 8px;
margin-bottom: 8px;
- background-color: $dropdown-divider-color;
+ background-color: $dropdown-divider-bg;
}
.dropdown-menu-empty-item a {
@@ -542,7 +542,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 +621,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 +656,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/filters.scss b/app/assets/stylesheets/framework/filters.scss
index d5693a5d1a1..f48b3ddc912 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -363,6 +363,12 @@
background-color: $white-light;
border-top: 0;
}
+
+ .filter-dropdown-container {
+ .dropdown {
+ margin-left: 0;
+ }
+ }
}
@include media-breakpoint-down(sm) {
@@ -372,16 +378,6 @@
.dropdown-menu {
width: 100%;
}
-
- .dropdown {
- margin-left: 0;
- }
-
- .fa-chevron-down {
- position: absolute;
- right: 10px;
- top: 10px;
- }
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 7a4c3914fb0..afa85f0e4ae 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -32,16 +32,16 @@
margin: 0;
}
+ .flash-text,
+ .flash-action {
+ display: inline-block;
+ }
+
.flash-alert {
@extend .alert;
background-color: $red-500;
margin: 0;
- .flash-text,
- .flash-action {
- display: inline-block;
- }
-
.flash-action {
margin-left: 5px;
text-decoration: none;
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/header.scss b/app/assets/stylesheets/framework/header.scss
index 39410ac56af..c0cda29e239 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -383,6 +383,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 {
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 452e946f95f..73533571a2f 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -42,11 +42,12 @@
padding: 10px;
text-align: right;
float: left;
+ line-height: 1;
a {
font-family: $monospace-font;
display: block;
- font-size: $code_font_size !important;
+ font-size: $code-font-size !important;
min-height: 19px;
white-space: nowrap;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index abd26e38d18..8db7d63266e 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -80,3 +80,15 @@
.user-avatar-link {
text-decoration: none;
}
+
+.circle-icon-container {
+ $border-size: 1px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: $border-size solid $theme-gray-400;
+ border-radius: 50%;
+ padding: $gl-padding-8 - $border-size;
+ color: $theme-gray-700;
+}
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/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index de9e7c37695..19640ab5986 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -158,6 +158,10 @@
width: 100%;
}
+ .dropdown-menu-toggle {
+ margin-bottom: 0;
+ }
+
form {
display: block;
height: auto;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 4a311da1675..3d5208c3db5 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -31,16 +31,6 @@
.timeline-entry-inner {
position: relative;
-
- @include notes-media('max', map-get($grid-breakpoints, sm)) {
- .timeline-icon {
- display: none;
- }
-
- .timeline-content {
- margin-left: 0;
- }
- }
}
&:target,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b43bb3feef5..134b3a4521b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -197,6 +197,7 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
+$gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
@@ -243,6 +244,7 @@ $gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
$input-horizontal-padding: 12px;
+$browserScrollbarSize: 10px;
/*
* Misc
@@ -269,7 +271,8 @@ $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;
@@ -331,7 +334,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);
@@ -365,6 +367,8 @@ $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: 13px;
/*
* Badges
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 759b4f333ca..fab1b361f14 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -19,3 +19,5 @@ $info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
+$nav-divider-margin-y: ($grid-size / 2);
+$dropdown-divider-bg: $theme-gray-200;
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/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/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b075009b57c..221b4e934ff 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -50,9 +50,19 @@
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
- border-radius: 4px;
+ border-radius: $border-radius-default;
+}
- &:not(.deploy-heading)::before {
+.mr-widget-extension {
+ border-top: 1px solid $border-color;
+ background-color: $gray-light;
+}
+
+.mr-widget-workflow {
+ margin-top: $gl-padding;
+ position: relative;
+
+ &::before {
content: '';
border-left: 1px solid $theme-gray-200;
position: absolute;
@@ -68,8 +78,8 @@
border-top: 0;
}
-.mr-widget-heading,
.mr-widget-section,
+.mr-widget-content,
.mr-widget-footer {
padding: $gl-padding;
}
@@ -560,19 +570,6 @@
color: $gl-text-color;
}
- .git-merge-icon-container {
- border: 1px solid $theme-gray-400;
- border-radius: 50%;
- height: 32px;
- width: 32px;
- color: $theme-gray-700;
- line-height: 28px;
-
- .ic-git-merge {
- vertical-align: middle;
- width: 31px;
- }
- }
.git-merge-container {
justify-content: space-between;
@@ -854,11 +851,6 @@
}
.deploy-heading {
- margin-top: -19px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- background-color: $gray-light;
-
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
@@ -868,6 +860,10 @@
font-size: 12px;
margin-left: 48px;
}
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $border-color;
+ }
}
.deploy-body {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4fda2964fd5..39d01c49fd7 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -371,10 +371,10 @@ $note-form-margin-left: 72px;
&::after {
content: '';
- width: 100%;
height: 70px;
position: absolute;
- left: 0;
+ left: $gl-padding-24;
+ right: 0;
bottom: 0;
background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%);
}
@@ -589,12 +589,6 @@ $note-form-margin-left: 72px;
padding-bottom: 0;
}
-.note-header-author-name {
- @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
- display: none;
- }
-}
-
.note-headline-light {
display: inline;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 1d691d1d8b8..132f3fea92b 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -144,11 +144,13 @@
.provider-btn-group {
display: inline-block;
margin-right: 10px;
+ margin-bottom: 10px;
border: 1px solid $border-color;
border-radius: 3px;
&:last-child {
margin-right: 0;
+ margin-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 80ec390d18e..278800aba95 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,15 +770,16 @@
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 {
margin-top: $gl-padding-8;
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding-8 - $browserScrollbarSize;
+ padding-bottom: $browserScrollbarSize;
flex-wrap: wrap;
border-bottom: 0;
}
@@ -731,7 +787,7 @@
.fade-left,
.fade-right {
top: 0;
- height: 100%;
+ height: calc(100% - #{$browserScrollbarSize});
.fa {
top: 50%;
@@ -785,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 {
@@ -810,6 +886,13 @@
}
}
+.project-buttons {
+ .stat-text {
+ @extend .btn;
+ @extend .btn-default;
+ }
+}
+
.repository-languages-bar {
height: 8px;
margin-bottom: $gl-padding-8;
@@ -933,8 +1016,6 @@ pre.light-well {
}
.git-clone-holder {
- width: 320px;
-
.btn-clipboard {
border: 1px solid $border-color;
}
@@ -957,6 +1038,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/tree.scss b/app/assets/stylesheets/pages/tree.scss
index dc5ca78ff58..a46b8679a42 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -104,11 +104,23 @@
border-bottom: 1px solid $white-normal;
border-top: 1px solid $white-normal;
+ &:last-of-type {
+ border-bottom-color: $white-light;
+ }
+
td,
th {
line-height: 21px;
}
+ th {
+ border-top-color: $gray-light;
+ }
+
+ td {
+ border-color: $border-color;
+ }
+
&:hover:not(.tree-truncated-warning) {
td {
background-color: $blue-50;
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 08d7e3b4fa2..65fe22bd8f4 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -5,23 +5,12 @@ class Admin::ImpersonationsController < Admin::ApplicationController
before_action :authenticate_impersonator!
def destroy
- original_user = current_user
-
- warden.set_user(impersonator, scope: :user)
-
- Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
-
- session[:impersonator_id] = nil
-
+ original_user = stop_impersonation
redirect_to admin_user_path(original_user), status: :found
end
private
- def impersonator
- @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
- end
-
def authenticate_impersonator!
render_404 unless impersonator && impersonator.admin? && !impersonator.blocked?
end
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
index 64d74ae4231..57f7d3e3951 100644
--- a/app/controllers/admin/requests_profiles_controller.rb
+++ b/app/controllers/admin/requests_profiles_controller.rb
@@ -11,7 +11,7 @@ class Admin::RequestsProfilesController < Admin::ApplicationController
profile = Gitlab::RequestProfiler::Profile.find(clean_name)
if profile
- render text: profile.content
+ render html: profile.content
else
redirect_to admin_requests_profiles_path, alert: 'Profile not found'
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index b783c0e2a6f..e93be1c1ba2 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -2,6 +2,7 @@
class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create]
+ before_action :check_impersonation_availability, only: :impersonate
def index
@users = User.order_name_asc.filter(params[:filter])
@@ -227,4 +228,8 @@ class Admin::UsersController < Admin::ApplicationController
result[:status] == :success
end
+
+ def check_impersonation_availability
+ access_denied! unless Gitlab.config.gitlab.impersonation_enabled
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9b40ffb26a2..7c8c1392c1c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,15 +8,14 @@ class ApplicationController < ActionController::Base
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
- include SentryHelper
include WorkhorseHelper
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
+ include SessionlessAuthentication
# this can be removed after switching to rails 5
# https://gitlab.com/gitlab-org/gitlab-ce/issues/51908
include InvalidUTF8ErrorHandler unless Gitlab.rails5?
- before_action :authenticate_sessionless_user!
before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
@@ -28,6 +27,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
+ before_action :check_impersonation_availability
around_action :set_locale
@@ -128,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
@@ -153,15 +154,8 @@ class ApplicationController < ActionController::Base
end
end
- # This filter handles personal access tokens, and atom requests with rss tokens
- def authenticate_sessionless_user!
- user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user
-
- sessionless_sign_in(user) if user
- 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
@@ -426,25 +420,11 @@ class ApplicationController < ActionController::Base
Gitlab::I18n.with_user_locale(current_user, &block)
end
- def sessionless_sign_in(user)
- if user && can?(user, :log_in)
- # Notice we are passing store false, so the user is not
- # actually stored in the session and a token is needed
- # for every request. If you want the token to work as a
- # sign in token, you can simply remove store: false.
- sign_in(user, store: false, message: :sessionless_sign_in)
- end
- end
-
def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
- def sessionless_user?
- current_user && !session.keys.include?('warden.user.user.key')
- end
-
def peek_request?
request.path.start_with?('/-/peek')
end
@@ -483,4 +463,32 @@ class ApplicationController < ActionController::Base
.new(settings, current_user, application_setting_params)
.execute
end
+
+ def check_impersonation_availability
+ return unless session[:impersonator_id]
+
+ unless Gitlab.config.gitlab.impersonation_enabled
+ stop_impersonation
+ access_denied! _('Impersonation has been disabled')
+ end
+ end
+
+ def stop_impersonation
+ impersonated_user = current_user
+
+ Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{impersonated_user.username}")
+
+ warden.set_user(impersonator, scope: :user)
+ session[:impersonator_id] = nil
+
+ impersonated_user
+ end
+
+ 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/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7f874687212..0dd7500623d 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -100,18 +100,12 @@ module Boards
.merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
end
+ def serializer
+ IssueSerializer.new(current_user: current_user)
+ end
+
def serialize_as_json(resource)
- resource.as_json(
- only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
- labels: true,
- issue_endpoints: true,
- include_full_project_path: board.group_board?,
- include: {
- project: { only: [:id, :path] },
- assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
- milestone: { only: [:id, :title] }
- }
- )
+ serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
end
def whitelist_query_limiting
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
index b4f46cddbe9..8d518c14b90 100644
--- a/app/controllers/chaos_controller.rb
+++ b/app/controllers/chaos_controller.rb
@@ -15,7 +15,7 @@ class ChaosController < ActionController::Base
duration_taken = (Time.now - start).seconds
Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def cpuspin
@@ -24,14 +24,14 @@ class ChaosController < ActionController::Base
rand while Time.now < end_time
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def sleep
duration_s = (params[:duration_s]&.to_i || 30).seconds
Kernel.sleep duration_s
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def kill
@@ -44,13 +44,13 @@ class ChaosController < ActionController::Base
secret = ENV['GITLAB_CHAOS_SECRET']
# GITLAB_CHAOS_SECRET is required unless you're running in Development mode
if !secret && !Rails.env.development?
- render text: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", content_type: 'text/plain', status: 500
+ render plain: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", status: :internal_server_error
end
return unless secret
unless request.headers["HTTP_X_CHAOS_SECRET"] == secret
- render text: "To experience chaos, please set X-Chaos-Secret header", content_type: 'text/plain', status: 401
+ render plain: "To experience chaos, please set X-Chaos-Secret header", status: :unauthorized
end
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 34a8c50fcbd..a597996a362 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -91,7 +91,7 @@ module IssuableCollections
options = {
scope: params[:scope],
state: params[:state],
- sort: set_sort_order_from_cookie || default_sort_order
+ sort: set_sort_order
}
# Used by view to highlight active option
@@ -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)
@@ -113,6 +113,32 @@ module IssuableCollections
'opened'
end
+ def set_sort_order
+ set_sort_order_from_user_preference || set_sort_order_from_cookie || default_sort_order
+ end
+
+ def set_sort_order_from_user_preference
+ return unless current_user
+ return unless issuable_sorting_field
+
+ user_preference = current_user.user_preference
+
+ sort_param = params[:sort]
+ sort_param ||= user_preference[issuable_sorting_field]
+
+ if user_preference[issuable_sorting_field] != sort_param
+ user_preference.update_attribute(issuable_sorting_field, sort_param)
+ end
+
+ sort_param
+ end
+
+ # Implement default_sorting_field method on controllers
+ # to choose which column to store the sorting parameter.
+ def issuable_sorting_field
+ nil
+ end
+
def set_sort_order_from_cookie
sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
@@ -141,12 +167,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/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 777b147e2dd..0319948a12f 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -6,6 +6,7 @@ module NotesActions
extend ActiveSupport::Concern
included do
+ prepend_before_action :normalize_create_params, only: [:create]
before_action :set_polling_interval_header, only: [:index]
before_action :require_noteable!, only: [:index, :create]
before_action :authorize_admin_note!, only: [:update, :destroy]
@@ -247,6 +248,15 @@ module NotesActions
DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity)
end
+ # Avoids checking permissions in the wrong object - this ensures that the object we checked permissions for
+ # is the object we're actually creating a note in.
+ def normalize_create_params
+ params[:note].try do |note|
+ note[:noteable_id] = params[:target_id]
+ note[:noteable_type] = params[:target_type].classify
+ end
+ end
+
def note_project
strong_memoize(:note_project) do
next nil unless project
diff --git a/app/controllers/concerns/sessionless_authentication.rb b/app/controllers/concerns/sessionless_authentication.rb
new file mode 100644
index 00000000000..590eefc6dab
--- /dev/null
+++ b/app/controllers/concerns/sessionless_authentication.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# == SessionlessAuthentication
+#
+# Controller concern to handle PAT and RSS token authentication methods
+#
+module SessionlessAuthentication
+ # This filter handles personal access tokens, and atom requests with rss tokens
+ def authenticate_sessionless_user!(request_format)
+ user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
+
+ sessionless_sign_in(user) if user
+ end
+
+ def sessionless_user?
+ current_user && !session.keys.include?('warden.user.user.key')
+ end
+
+ def sessionless_sign_in(user)
+ if user && can?(user, :log_in)
+ # Notice we are passing store false, so the user is not
+ # actually stored in the session and a token is needed
+ # for every request. If you want the token to work as a
+ # sign in token, you can simply remove store: false.
+ sign_in(user, store: false, message: :sessionless_sign_in)
+ end
+ 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 7a1c7abfb8f..0eea0cdd50f 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,17 +1,11 @@
# frozen_string_literal: true
module UploadsActions
- extend ActiveSupport::Concern
-
include Gitlab::Utils::StrongMemoize
include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
- included do
- prepend_before_action :set_html_format, only: :show
- end
-
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -44,6 +38,7 @@ module UploadsActions
return render_404 unless uploader
+ workhorse_set_content_type!
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
@@ -61,13 +56,6 @@ module UploadsActions
private
- # Explicitly set the format.
- # Otherwise rails 5 will set it from a file extension.
- # See https://github.com/rails/rails/commit/84e8accd6fb83031e4c27e44925d7596655285f7#diff-2b8f2fbb113b55ca8e16001c393da8f1
- def set_html_format
- request.format = :html
- end
-
def uploader_class
raise NotImplementedError
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index e9686ed8d06..57e612d89d3 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
+ prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
before_action :default_sorting
skip_cross_project_access_check :index, :starred
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index b82caf30a91..3fa582cf25b 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -4,6 +4,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
before_action :authorize_read_project!, only: :index
+ before_action :authorize_read_group!, only: :index
before_action :find_todos, only: [:index, :destroy_all]
def index
@@ -60,6 +61,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
end
+ def authorize_read_group!
+ group_id = params[:group_id]
+
+ if group_id.present?
+ group = Group.find(group_id)
+ render_404 unless can?(current_user, :read_group, group)
+ end
+ end
+
def find_todos
@todos ||= TodosFinder.new(current_user, todo_params).execute
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4ce9be44403..be2d9512c01 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -4,6 +4,9 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction
include MergeRequestsAction
+ prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) }
+ prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
+
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests]
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index a1ec144410b..6ea4758ec32 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -3,6 +3,7 @@
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
+ prepend_before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :check_graphql_feature_flag!
diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
index 50c44b7a58b..b846fb21266 100644
--- a/app/controllers/groups/clusters_controller.rb
+++ b/app/controllers/groups/clusters_controller.rb
@@ -3,8 +3,8 @@
class Groups::ClustersController < Clusters::ClustersController
include ControllerWithCrossProjectAccessCheck
- prepend_before_action :check_group_clusters_feature_flag!
prepend_before_action :group
+ prepend_before_action :check_group_clusters_feature_flag!
requires_cross_project_access
layout 'group'
@@ -20,6 +20,10 @@ class Groups::ClustersController < Clusters::ClustersController
end
def check_group_clusters_feature_flag!
- render_404 unless Feature.enabled?(:group_clusters)
+ render_404 unless group_clusters_enabled?
+ end
+
+ def group_clusters_enabled?
+ group.group_clusters_enabled?
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 062c8c4e9e1..c5d8ac2ed77 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -9,6 +9,9 @@ class GroupsController < Groups::ApplicationController
respond_to :html
+ prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
+ prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
+
before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create]
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 7353be478e1..c2089a0fca3 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -15,7 +15,7 @@ class MetricsController < ActionController::Base
"# Metrics are disabled, see: #{help_page}\n"
end
- render text: response, content_type: 'text/plain; version=0.0.4'
+ render plain: response, content_type: 'text/plain; version=0.0.4'
end
private
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/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index b50f140dc80..ab4ca56bb49 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -9,7 +9,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
before_action :verify_user_oauth_applications_enabled, except: :index
before_action :authenticate_user!
before_action :add_gon_variables
- before_action :load_scopes, only: [:index, :create, :edit]
+ before_action :load_scopes, only: [:index, :create, :edit, :update]
helper_method :can?
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index cb3180f4196..b0d65f284af 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -4,7 +4,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
include AuthHelper
def show
- @user = current_user
+ render(locals: show_view_variables)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -23,4 +23,10 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def show_view_variables
+ {}
+ end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 912421e3d08..dcee8eb7e6e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -41,12 +41,12 @@ class Profiles::KeysController < Profiles::ApplicationController
user = UserFinder.new(params[:username]).find_by_username
if user.present?
headers['Content-Disposition'] = 'attachment'
- render text: user.all_ssh_keys.join("\n"), content_type: 'text/plain'
+ render plain: user.all_ssh_keys.join("\n")
else
return render_404
end
rescue => e
- render text: e.message
+ render html: e.message
end
else
return render_404
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index ae9c17802b9..1a91e07b97f 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -9,7 +9,6 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
- before_action :set_request_format, only: [:file]
before_action :validate_artifacts!, except: [:download]
before_action :entry, only: [:file]
@@ -110,12 +109,4 @@ class Projects::ArtifactsController < Projects::ApplicationController
render_404 unless @entry.exists?
end
-
- def set_request_format
- request.format = :html if set_request_format?
- end
-
- def set_request_format?
- request.format != :json
- end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 873c96a5523..60fabd15333 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -9,7 +9,6 @@ class Projects::BlobController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
prepend_before_action :authenticate_user!, only: [:edit]
- before_action :set_request_format, only: [:edit, :show, :update, :destroy]
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
@@ -242,18 +241,6 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
end
- # In Rails 4.2 if params[:format] is empty, Rails set it to :html
- # But since Rails 5.0 the framework now looks for an extension.
- # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md`
- # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests.
- def set_request_format
- request.format = :html if set_request_format?
- end
-
- def set_request_format?
- params[:id].present? && params[:format].blank? && request.format != "json"
- end
-
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 95a014d24da..a6bfb913900 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -22,7 +22,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Fetch branches for the specified mode
fetch_branches_by_mode
- @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+ @refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitaly/issues/992
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 84a2a461da7..e40a1a1d744 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -6,12 +6,12 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
+ prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :whitelist_query_limiting, except: :commits_root
before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code!
before_action :set_commits, except: :commits_root
- before_action :set_request_format, only: :show
def commits_root
redirect_to project_commits_path(@project, @project.default_branch)
@@ -70,19 +70,6 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = set_commits_for_rendering(@commits)
end
- # Rails 5 sets request.format from the extension.
- # Explicitly set to :html.
- def set_request_format
- request.format = :html if set_request_format?
- end
-
- # Rails 5 sets request.format from extension.
- # In this case if the ref ends with `.atom`, it's expected to be the html response,
- # not the atom one. So explicitly set request.format as :html to act like rails4.
- def set_request_format?
- request.format.to_s == "text/html" || @commits.ref.ends_with?("atom")
- end
-
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index de10783df1a..e940f382a19 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -122,7 +122,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
- render text: 'Not found', status: :not_found
+ render html: 'Not found', status: :not_found
end
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index e55065c5817..a10e159ea1e 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -13,10 +13,8 @@ class Projects::ImportsController < Projects::ApplicationController
end
def create
- @project.import_url = params[:project][:import_url]
-
- if @project.save
- @project.reload.import_schedule
+ if @project.update(safe_import_params)
+ @project.import_state.reload.schedule
end
redirect_to project_import_path(@project)
@@ -24,7 +22,7 @@ class Projects::ImportsController < Projects::ApplicationController
def show
if @project.import_finished?
- if continue_params
+ if continue_params&.key?(:to)
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to project_path(@project), notice: finished_notice
@@ -67,4 +65,12 @@ class Projects::ImportsController < Projects::ApplicationController
redirect_to project_path(@project)
end
end
+
+ def import_params
+ params.require(:project).permit(:import_url)
+ end
+
+ def safe_import_params
+ import_params
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d6d7110355b..c6ab6b4642e 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -9,10 +9,6 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar
include SpammableActions
- def self.authenticate_user_only_actions
- %i[new]
- end
-
def self.issue_except_actions
%i[index calendar new create bulk_update]
end
@@ -21,7 +17,10 @@ class Projects::IssuesController < Projects::ApplicationController
%i[index calendar]
end
- prepend_before_action :authenticate_user!, only: authenticate_user_only_actions
+ prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
+ prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
+ prepend_before_action :authenticate_new_issue!, only: [:new]
+ prepend_before_action :store_uri, only: [:new, :show]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
@@ -232,16 +231,18 @@ class Projects::IssuesController < Projects::ApplicationController
] + [{ label_ids: [], assignee_ids: [] }]
end
- def authenticate_user!
+ def authenticate_new_issue!
return if current_user
notice = "Please sign in to create the new issue."
+ redirect_to new_user_session_path, notice: notice
+ end
+
+ def store_uri
if request.get? && !request.xhr?
store_location_for :user, request.fullpath
end
-
- redirect_to new_user_session_path, notice: notice
end
def serializer
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/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index b3d77335c2a..ddffbb17ace 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -22,12 +22,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
- notes_grouped_by_path = renderable_notes.group_by { |note| note.position.file_path }
- @diffs.diff_files.each do |diff_file|
- notes = notes_grouped_by_path.fetch(diff_file.file_path, [])
- notes.each { |note| diff_file.unfold_diff_lines(note.position) }
- end
+ note_positions = renderable_notes.map(&:position).compact
+ @diffs.unfold_diff_files(note_positions)
@diffs.write_cache
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/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 20998c97730..8e68014a30d 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -11,7 +11,10 @@ class Projects::MilestonesController < Projects::ApplicationController
before_action :authorize_read_milestone!
# Allow admin milestone
- before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels, :promote]
+ before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
+
+ # Allow to promote milestone
+ before_action :authorize_promote_milestone!, only: :promote
respond_to :html
@@ -78,7 +81,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = flash_notice_for(promoted_milestone, project.group)
+ flash[:notice] = flash_notice_for(promoted_milestone, project_group)
respond_to do |format|
format.html do
@@ -109,6 +112,12 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
+ def project_group
+ strong_memoize(:project_group) do
+ project.group
+ end
+ end
+
def milestones
strong_memoize(:milestones) do
MilestonesFinder.new(search_params).execute
@@ -125,13 +134,17 @@ class Projects::MilestonesController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_milestone, @project)
end
+ def authorize_promote_milestone!
+ return render_404 unless can?(current_user, :admin_milestone, project_group)
+ end
+
def milestone_params
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
def search_params
- if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
- groups = @project.group.self_and_ancestors_ids
+ if request.format.json? && project_group && can?(current_user, :read_group, project_group)
+ groups = project_group.self_and_ancestors_ids
end
params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index ad2466a8588..6543711ecfa 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -8,6 +8,7 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
+ before_action :assign_options
before_action :assign_commit
def show
@@ -29,10 +30,13 @@ class Projects::NetworkController < Projects::ApplicationController
render
end
+ def assign_options
+ @options = params.permit(:filter_ref, :extended_sha1)
+ end
+
def assign_commit
- return if params[:extended_sha1].blank?
+ return if @options[:extended_sha1].blank?
- @options[:extended_sha1] = params[:extended_sha1]
@commit = @repo.commit(@options[:extended_sha1])
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 53b29d4146e..67827b1d3bb 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -46,7 +46,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def new
- @pipeline = project.pipelines.new(ref: @project.default_branch)
+ @pipeline = project.all_pipelines.new(ref: @project.default_branch)
end
def create
@@ -142,9 +142,9 @@ class Projects::PipelinesController < Projects::ApplicationController
@charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {}
- @counts[:total] = @project.pipelines.count(:all)
- @counts[:success] = @project.pipelines.success.count(:all)
- @counts[:failed] = @project.pipelines.failed.count(:all)
+ @counts[:total] = @project.all_pipelines.count(:all)
+ @counts[:success] = @project.all_pipelines.success.count(:all)
+ @counts[:failed] = @project.all_pipelines.failed.count(:all)
end
private
@@ -164,7 +164,7 @@ class Projects::PipelinesController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def pipeline
@pipeline ||= project
- .pipelines
+ .all_pipelines
.includes(user: :status)
.find_by!(id: params[:id])
.present(current_user: current_user)
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/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index c8442ff3592..686d66b10a3 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -3,6 +3,8 @@
class Projects::TagsController < Projects::ApplicationController
include SortingHelper
+ prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
+
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
@@ -18,7 +20,7 @@ class Projects::TagsController < Projects::ApplicationController
@tags = Kaminari.paginate_array(@tags).page(params[:page])
tag_names = @tags.map(&:name)
- @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
+ @tags_pipelines = @project.ci_pipelines.latest_successful_for_refs(tag_names)
@releases = project.releases.where(tag: tag_names)
respond_to do |format|
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 7f4a9f5151b..8bf93bfd68d 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,6 +7,8 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown
include SendFileUpload
+ prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
+
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show]
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 5b70c69d7f4..8b040dc080e 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -14,6 +14,7 @@ class UsersController < ApplicationController
calendar_activities: true
skip_before_action :authenticate_user!
+ prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
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/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 35d0e1acce5..f5aadc42ff0 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -8,7 +8,7 @@ class PipelinesFinder
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
- @pipelines = project.pipelines
+ @pipelines = project.all_pipelines
@params = params
end
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/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/auth_helper.rb b/app/helpers/auth_helper.rb
index 44f85e9c0f8..654fb9d9987 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -57,6 +57,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
+ def display_providers_on_profile?
+ button_based_providers.any?
+ end
+
def providers_for_base_controller
auth_providers.reject { |provider| LDAP_PROVIDER === provider }
end
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/groups_helper.rb b/app/helpers/groups_helper.rb
index e9b9b9b7721..866fc555856 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -140,7 +140,7 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
- if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
+ if can?(current_user, :read_cluster, @group) && @group.group_clusters_enabled?
links << :kubernetes
end
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/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 94a030d9d57..9666080092b 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -2,6 +2,7 @@
module MilestonesHelper
include EntityDateHelper
+ include Gitlab::Utils::StrongMemoize
def milestones_filter_path(opts = {})
if @project
@@ -243,4 +244,16 @@ module MilestonesHelper
dashboard_milestone_path(milestone.safe_title, title: milestone.title)
end
end
+
+ def can_admin_project_milestones?
+ strong_memoize(:can_admin_project_milestones) do
+ can?(current_user, :admin_milestone, @project)
+ end
+ end
+
+ def can_admin_group_milestones?
+ strong_memoize(:can_admin_group_milestones) do
+ can?(current_user, :admin_milestone, @project.group)
+ end
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 0a7f930110a..7ce6b04df7e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -257,6 +257,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 +311,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,
@@ -545,6 +550,7 @@ module ProjectsHelper
%w[
environments
clusters
+ functions
user
gcp
]
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 8ed2a2ec9f4..f51b96ba8ce 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -120,10 +120,69 @@ module SortingHelper
}
end
+ def users_sort_options_hash
+ {
+ sort_value_name => sort_title_name,
+ sort_value_recently_signin => sort_title_recently_signin,
+ sort_value_oldest_signin => sort_title_oldest_signin,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_updated => sort_title_recently_updated,
+ sort_value_oldest_updated => sort_title_oldest_updated
+ }
+ end
+
def sortable_item(item, path, sorted_by)
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)
+ 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/users_helper.rb b/app/helpers/users_helper.rb
index 42b533ad772..bde9ca0cbf2 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -70,6 +70,10 @@ module UsersHelper
end
end
+ def impersonation_enabled?
+ Gitlab.config.gitlab.impersonation_enabled
+ end
+
private
def get_profile_tabs
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/notes.rb b/app/mailers/emails/notes.rb
index d3284e90568..1b3c1f9a8a9 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -26,7 +26,7 @@ module Emails
mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id))
end
- def note_snippet_email(recipient_id, note_id)
+ def note_project_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable
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/models/application_setting.rb b/app/models/application_setting.rb
index 207ffae873a..4319db42019 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base
include IgnorableColumn
include ChronicDurationAttribute
- add_authentication_token_field :runners_registration_token
+ add_authentication_token_field :runners_registration_token, encrypted: true, fallback: true
add_authentication_token_field :health_check_access_token
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d60861dc95f..d86a6eceb59 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
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7c84bd734bb..da08214963f 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -15,6 +15,8 @@ module Ci
WRITE_LOCK_SLEEP = 0.01.seconds
WRITE_LOCK_TTL = 1.minute
+ FailedToPersistDataError = Class.new(StandardError)
+
# Note: The ordering of this enum is related to the precedence of persist store.
# The bottom item takes the higest precedence, and the top item takes the lowest precedence.
enum data_store: {
@@ -109,16 +111,19 @@ module Ci
def unsafe_persist_to!(new_store)
return if data_store == new_store.to_s
- raise ArgumentError, 'Can not persist empty data' unless size > 0
- old_store_class = self.class.get_store_class(data_store)
+ current_data = get_data
- get_data.tap do |the_data|
- self.raw_data = nil
- self.data_store = new_store
- unsafe_set_data!(the_data)
+ unless current_data&.bytesize.to_i == CHUNK_SIZE
+ raise FailedToPersistDataError, 'Data is not fullfilled in a bucket'
end
+ old_store_class = self.class.get_store_class(data_store)
+
+ self.raw_data = nil
+ self.data_store = new_store
+ unsafe_set_data!(current_data)
+
old_store_class.delete_data(self)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 9512ba42f67..d06022a0fb7 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -12,13 +12,14 @@ module Ci
include AtomicInternalId
include EnumWithNil
- belongs_to :project, inverse_of: :pipelines
+ belongs_to :project, inverse_of: :all_pipelines
belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
+ belongs_to :merge_request, class_name: 'MergeRequest'
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
- s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
+ s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
end
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
@@ -26,6 +27,8 @@ module Ci
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
+ has_many :deployments, through: :builds
+ has_many :environments, -> { distinct }, through: :deployments
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
@@ -48,6 +51,9 @@ module Ci
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
+ validates :merge_request, presence: { if: :merge_request? }
+ validates :merge_request, absence: { unless: :merge_request? }
+ validates :tag, inclusion: { in: [false], if: :merge_request? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
@@ -168,6 +174,16 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
+ scope :ci_sources, -> { where(config_source: ci_sources_values) }
+
+ scope :sort_by_merge_request_pipelines, -> do
+ sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
+ query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend
+
+ order(query)
+ end
+
+ scope :for_user, -> (user) { where(user: user) }
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
@@ -256,6 +272,10 @@ module Ci
sources.reject { |source| source == "external" }.values
end
+ def self.ci_sources_values
+ config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
@@ -368,7 +388,7 @@ module Ci
end
def branch?
- !tag?
+ !tag? && !merge_request?
end
def stuck?
@@ -494,6 +514,8 @@ module Ci
end
def ci_yaml_file_path
+ return unless repository_source? || unknown_source?
+
if project.ci_config_path.blank?
'.gitlab-ci.yml'
else
@@ -523,10 +545,6 @@ module Ci
yaml_errors.present?
end
- def environments
- builds.where.not(environment: nil).success.pluck(:environment).uniq
- end
-
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
@@ -587,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
@@ -617,7 +640,12 @@ module Ci
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
- @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
+ @all_merge_requests ||=
+ if merge_request?
+ project.merge_requests.where(id: merge_request.id)
+ else
+ project.merge_requests.where(source_branch: ref)
+ end
end
def detailed_status(current_user)
@@ -666,6 +694,7 @@ module Ci
def ci_yaml_from_repo
return unless project
return unless sha
+ return unless ci_yaml_file_path
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue GRPC::NotFound, GRPC::Internal
@@ -693,6 +722,8 @@ module Ci
def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
+ elsif merge_request?
+ Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 8d8d16e2ec1..c0f16066e0b 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -21,7 +21,8 @@ module Ci
trigger: 3,
schedule: 4,
api: 5,
- external: 6
+ external: 6,
+ merge_request: 10
}
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 31330d0682e..2693386443a 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -8,6 +8,9 @@ module Ci
include RedisCacheable
include ChronicDurationAttribute
include FromUnion
+ include TokenAuthenticatable
+
+ add_authentication_token_field :token, encrypted: true, migrating: true
enum access_level: {
not_protected: 0,
@@ -39,7 +42,7 @@ module Ci
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
- before_validation :set_default_values
+ before_save :ensure_token
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
@@ -111,7 +114,8 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
- chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
+ chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
+ error_message: 'Maximum job timeout has a value which could not be accepted'
validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
@@ -145,10 +149,6 @@ module Ci
end
end
- def set_default_values
- self.token = SecureRandom.hex(15) if self.token.blank?
- end
-
def assign_to(project, current_user = nil)
if instance_type?
self.runner_type = :project_type
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index e43a0fd1786..421a923d386 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -56,7 +56,11 @@ module Clusters
def specification
{
"ingress" => {
- "hosts" => [hostname]
+ "hosts" => [hostname],
+ "tls" => [{
+ "hosts" => [hostname],
+ "secretName" => "jupyter-cert"
+ }]
},
"hub" => {
"extraEnv" => {
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 13906c903b9..7fe43cd2de0 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -4,6 +4,7 @@ module Clusters
class Cluster < ActiveRecord::Base
include Presentable
include Gitlab::Utils::StrongMemoize
+ include FromUnion
self.table_name = 'clusters'
@@ -86,6 +87,29 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
+ scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
+ subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.cluster_id = clusters.id')
+
+ 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
+
+ hierarchy_groups.flat_map(&:clusters)
+ end
+
def status_name
if provider
provider.status_name
@@ -122,6 +146,16 @@ module Clusters
!user?
end
+ def all_projects
+ if project_type?
+ projects
+ elsif group_type?
+ first_group.all_projects
+ else
+ Project.none
+ end
+ end
+
def first_project
strong_memoize(:first_project) do
projects.first
@@ -140,11 +174,17 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes?
end
- def find_or_initialize_kubernetes_namespace(cluster_project)
- kubernetes_namespaces.find_or_initialize_by(
- project: cluster_project.project,
- cluster_project: cluster_project
- )
+ def find_or_initialize_kubernetes_namespace_for_project(project)
+ if project_type?
+ kubernetes_namespaces.find_or_initialize_by(
+ project: project,
+ cluster_project: cluster_project
+ )
+ else
+ kubernetes_namespaces.find_or_initialize_by(
+ project: project
+ )
+ end
end
def allow_user_defined_namespace?
diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb
index 34f5e38ff79..73da6cb37d7 100644
--- a/app/models/clusters/kubernetes_namespace.rb
+++ b/app/models/clusters/kubernetes_namespace.rb
@@ -33,14 +33,12 @@ module Clusters
end
def predefined_variables
- config = YAML.dump(kubeconfig)
-
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name.to_s)
.append(key: 'KUBE_NAMESPACE', value: namespace.to_s)
.append(key: 'KUBE_TOKEN', value: service_account_token.to_s, public: false)
- .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 3c5d7756eec..867f0edcb07 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -85,18 +85,16 @@ module Clusters
if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project)
variables.concat(kubernetes_namespace.predefined_variables)
- else
+ elsif cluster.project_type?
# From 11.5, every Clusters::Project should have at least one
# Clusters::KubernetesNamespace, so once migration has been completed,
# this 'else' branch will be removed. For more information, please see
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433
- config = YAML.dump(kubeconfig)
-
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
- .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 546fcc54a15..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
@@ -298,7 +300,7 @@ class Commit
end
def pipelines
- project.pipelines.where(sha: sha)
+ project.ci_pipelines.where(sha: sha)
end
def last_pipeline
@@ -312,7 +314,7 @@ class Commit
end
def status_for_project(ref, pipeline_project)
- pipeline_project.pipelines.latest_status_per_commit(id, ref)[id]
+ pipeline_project.ci_pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index dd93af9df64..e349f0fe971 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -24,7 +24,7 @@ class CommitCollection
# Setting this status ahead of time removes the need for running a query for
# every commit we're displaying.
def with_pipeline_status
- statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
+ statuses = project.ci_pipelines.latest_status_per_commit(map(&:id), ref)
each do |commit|
commit.set_status_for_ref(ref, statuses[commit.id])
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/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 6e2adc76ec6..a8c9e54f00c 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -15,7 +15,7 @@ module CacheMarkdownField
# Increment this number every time the renderer changes its output
CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 11
+ CACHE_COMMONMARK_VERSION = 12
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
index edf6ac96730..af4905115b1 100644
--- a/app/models/concerns/chronic_duration_attribute.rb
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -24,7 +24,7 @@ module ChronicDurationAttribute
end
end
- validates virtual_attribute, allow_nil: true, duration: true
+ validates virtual_attribute, allow_nil: true, duration: { message: parameters[:error_message] }
end
alias_method :chronic_duration_attr, :chronic_duration_attr_writer
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index e57a3383544..0107af5f8ec 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -13,6 +13,7 @@ module DeploymentPlatform
def find_deployment_platform(environment)
find_cluster_platform_kubernetes(environment: environment) ||
+ find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) ||
find_kubernetes_service_integration ||
build_cluster_and_deployment_platform
end
@@ -23,6 +24,18 @@ module DeploymentPlatform
.last&.platform_kubernetes
end
+ def find_group_cluster_platform_kubernetes_with_feature_guard(environment: nil)
+ return unless group_clusters_enabled?
+
+ find_group_cluster_platform_kubernetes(environment: environment)
+ end
+
+ # EE would override this and utilize environment argument
+ def find_group_cluster_platform_kubernetes(environment: nil)
+ Clusters::Cluster.enabled.default_environment.ancestor_clusters_for_clusterable(self)
+ .first&.platform_kubernetes
+ end
+
def find_kubernetes_service_integration
services.deployment.reorder(nil).find_by(active: true)
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/shardable.rb b/app/models/concerns/shardable.rb
new file mode 100644
index 00000000000..57cd77b44b4
--- /dev/null
+++ b/app/models/concerns/shardable.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Shardable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :shard
+ validates :shard, presence: true
+ end
+
+ def shard_name
+ shard&.name
+ end
+
+ def shard_name=(name)
+ self.shard = Shard.by_name(name)
+ end
+end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 23a43aec677..f5bb559ceda 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -9,24 +9,18 @@ module TokenAuthenticatable
private # rubocop:disable Lint/UselessAccessModifier
def add_authentication_token_field(token_field, options = {})
- @token_fields = [] unless @token_fields
- unique = options.fetch(:unique, true)
-
- if @token_fields.include?(token_field)
+ if token_authenticatable_fields.include?(token_field)
raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
end
- @token_fields << token_field
+ token_authenticatable_fields.push(token_field)
attr_accessor :cleartext_tokens
- strategy = if options[:digest]
- TokenAuthenticatableStrategies::Digest.new(self, token_field, options)
- else
- TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
- end
+ strategy = TokenAuthenticatableStrategies::Base
+ .fabricate(self, token_field, options)
- if unique
+ if options.fetch(:unique, true)
define_singleton_method("find_by_#{token_field}") do |token|
strategy.find_token_authenticatable(token)
end
@@ -53,6 +47,15 @@ module TokenAuthenticatable
define_method("reset_#{token_field}!") do
strategy.reset_token!(self)
end
+
+ define_method("#{token_field}_matches?") do |other_token|
+ token = read_attribute(token_field)
+ token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(other_token, token)
+ end
+ end
+
+ def token_authenticatable_fields
+ @token_authenticatable_fields ||= []
end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index 413721d3e6c..01fb194281a 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -2,6 +2,8 @@
module TokenAuthenticatableStrategies
class Base
+ attr_reader :klass, :token_field, :options
+
def initialize(klass, token_field, options)
@klass = klass
@token_field = token_field
@@ -22,6 +24,7 @@ module TokenAuthenticatableStrategies
def ensure_token(instance)
write_new_token(instance) unless token_set?(instance)
+ get_token(instance)
end
# Returns a token, but only saves when the database is in read & write mode
@@ -36,6 +39,36 @@ module TokenAuthenticatableStrategies
instance.save! if Gitlab::Database.read_write?
end
+ def fallback?
+ unless options[:fallback].in?([true, false, nil])
+ raise ArgumentError, 'fallback: needs to be a boolean value!'
+ end
+
+ options[:fallback] == true
+ end
+
+ def migrating?
+ unless options[:migrating].in?([true, false, nil])
+ raise ArgumentError, 'migrating: needs to be a boolean value!'
+ end
+
+ options[:migrating] == true
+ end
+
+ def self.fabricate(model, field, options)
+ if options[:digest] && options[:encrypted]
+ raise ArgumentError, 'Incompatible options set!'
+ end
+
+ if options[:digest]
+ TokenAuthenticatableStrategies::Digest.new(model, field, options)
+ elsif options[:encrypted]
+ TokenAuthenticatableStrategies::Encrypted.new(model, field, options)
+ else
+ TokenAuthenticatableStrategies::Insecure.new(model, field, options)
+ end
+ end
+
protected
def write_new_token(instance)
@@ -65,9 +98,5 @@ module TokenAuthenticatableStrategies
def token_set?(instance)
raise NotImplementedError
end
-
- def token_field_name
- @token_field
- end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
new file mode 100644
index 00000000000..152491aa6e9
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Encrypted < Base
+ def initialize(*)
+ super
+
+ if migrating? && fallback?
+ raise ArgumentError, '`fallback` and `migrating` options are not compatible!'
+ end
+ end
+
+ def find_token_authenticatable(token, unscoped = false)
+ return if token.blank?
+
+ if fully_encrypted?
+ return find_by_encrypted_token(token, unscoped)
+ end
+
+ if fallback?
+ find_by_encrypted_token(token, unscoped) ||
+ find_by_plaintext_token(token, unscoped)
+ elsif migrating?
+ find_by_plaintext_token(token, unscoped)
+ else
+ raise ArgumentError, 'Unknown encryption phase!'
+ end
+ end
+
+ def ensure_token(instance)
+ # TODO, tech debt, because some specs are testing migrations, but are still
+ # using factory bot to create resources, it might happen that a database
+ # schema does not have "#{token_name}_encrypted" field yet, however a bunch
+ # of models call `ensure_#{token_name}` in `before_save`.
+ #
+ # In that case we are using insecure strategy, but this should only happen
+ # in tests, because otherwise `encrypted_field` is going to exist.
+ #
+ # Another use case is when we are caching resources / columns, like we do
+ # in case of ApplicationSetting.
+
+ return super if instance.has_attribute?(encrypted_field)
+
+ if fully_encrypted?
+ raise ArgumentError, 'Using encrypted strategy when encrypted field is missing!'
+ else
+ insecure_strategy.ensure_token(instance)
+ end
+ end
+
+ def get_token(instance)
+ return insecure_strategy.get_token(instance) if migrating?
+
+ encrypted_token = instance.read_attribute(encrypted_field)
+ token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+
+ token || (insecure_strategy.get_token(instance) if fallback?)
+ end
+
+ def set_token(instance, token)
+ raise ArgumentError unless token.present?
+
+ instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ instance[token_field] = token if migrating?
+ instance[token_field] = nil if fallback?
+ token
+ end
+
+ def fully_encrypted?
+ !migrating? && !fallback?
+ end
+
+ protected
+
+ def find_by_plaintext_token(token, unscoped)
+ insecure_strategy.find_token_authenticatable(token, unscoped)
+ end
+
+ def find_by_encrypted_token(token, unscoped)
+ encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+ relation(unscoped).find_by(encrypted_field => encrypted_value)
+ end
+
+ def insecure_strategy
+ @insecure_strategy ||= TokenAuthenticatableStrategies::Insecure
+ .new(klass, token_field, options)
+ end
+
+ def token_set?(instance)
+ raw_token = instance.read_attribute(encrypted_field)
+
+ unless fully_encrypted?
+ raw_token ||= insecure_strategy.get_token(instance)
+ end
+
+ raw_token.present?
+ end
+
+ def encrypted_field
+ @encrypted_field ||= "#{@token_field}_encrypted"
+ end
+ end
+end
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/environment_status.rb b/app/models/environment_status.rb
index 7078496ff52..2fb6cadc8cd 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -8,16 +8,17 @@ class EnvironmentStatus
delegate :id, to: :environment
delegate :name, to: :environment
delegate :project, to: :environment
+ delegate :status, to: :deployment, allow_nil: true
delegate :deployed_at, to: :deployment, allow_nil: true
def self.for_merge_request(mr, user)
- build_environments_status(mr, user, mr.diff_head_sha)
+ build_environments_status(mr, user, mr.actual_head_pipeline)
end
def self.after_merge_request(mr, user)
return [] unless mr.merged?
- build_environments_status(mr, user, mr.merge_commit_sha)
+ build_environments_status(mr, user, mr.merge_pipeline)
end
def initialize(environment, merge_request, sha)
@@ -43,22 +44,6 @@ class EnvironmentStatus
.merge_request_diff_files.where(deleted_file: false)
end
- ##
- # Since frontend has not supported all statuses yet, BE has to
- # proxy some status to a supported status.
- def status
- return unless deployment
-
- case deployment.status
- when 'created'
- 'running'
- when 'canceled'
- 'failed'
- else
- deployment.status
- end
- end
-
private
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
@@ -76,13 +61,13 @@ class EnvironmentStatus
}
end
- def self.build_environments_status(mr, user, sha)
- Environment.where(project_id: [mr.source_project_id, mr.target_project_id])
- .available
- .with_deployment(sha).map do |environment|
+ def self.build_environments_status(mr, user, pipeline)
+ return [] unless pipeline
+
+ pipeline.environments.available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
- EnvironmentStatus.new(environment, mr, sha)
+ EnvironmentStatus.new(environment, mr, pipeline.sha)
end.compact
end
private_class_method :build_environments_status
diff --git a/app/models/group.rb b/app/models/group.rb
index adb9169cfcd..233747cc2c2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -55,7 +55,7 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
- add_authentication_token_field :runners_token
+ add_authentication_token_field :runners_token, encrypted: true, migrating: true
after_create :post_create_hook
after_destroy :post_destroy_hook
@@ -400,6 +400,10 @@ class Group < Namespace
ensure_runners_token!
end
+ def group_clusters_enabled?
+ Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index b2fb79bc7ed..1a8662db9fb 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -6,12 +6,12 @@ class WebHook < ActiveRecord::Base
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_truncated
+ key: Settings.attr_encrypted_db_key_base_32
attr_encrypted :url,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_truncated
+ key: Settings.attr_encrypted_db_key_base_32
has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 780035c77e2..b7e13bcbccf 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -235,20 +235,6 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- if options.key?(:issue_endpoints) && project
- url_helper = Gitlab::Routing.url_helpers
-
- issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference
-
- json.merge!(
- reference_path: issue_reference,
- real_path: url_helper.project_issue_path(project, self),
- issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
- toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self),
- assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true)
- )
- end
-
if options.key?(:labels)
json[:labels] = labels.as_json(
project: project,
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/merge_request.rb b/app/models/merge_request.rb
index 92add079a02..861211ffc0a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -63,6 +63,7 @@ class MergeRequest < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
+ has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
belongs_to :assignee, class_name: "User"
@@ -538,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
@@ -1052,18 +1064,59 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def all_pipelines
+ def all_pipelines(shas: all_commit_shas)
return Ci::Pipeline.none unless source_project
- @all_pipelines ||= source_project.pipelines
- .where(sha: all_commit_shas, ref: source_branch)
- .order(id: :desc)
+ @all_pipelines ||= source_project.ci_pipelines
+ .where(sha: shas, ref: source_branch)
+ .where(merge_request: [nil, self])
+ .sort_by_merge_request_pipelines
+ end
+
+ def merge_request_pipeline_exists?
+ merge_request_pipelines.exists?(sha: diff_head_sha)
end
def has_test_reports?
actual_head_pipeline&.has_test_reports?
end
+ 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
+
# rubocop: disable CodeReuse/ServiceClass
def compare_test_reports
unless has_test_reports?
@@ -1214,7 +1267,7 @@ class MergeRequest < ActiveRecord::Base
end
def base_pipeline
- @base_pipeline ||= project.pipelines
+ @base_pipeline ||= project.ci_pipelines
.order(id: :desc)
.find_by(sha: diff_base_sha)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 11b03846f0b..8865c164b11 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -192,9 +192,9 @@ class Namespace < ActiveRecord::Base
# returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
- def ancestors_upto(top = nil)
+ def ancestors_upto(top = nil, hierarchy_order: nil)
Gitlab::GroupHierarchy.new(self.class.where(id: id))
- .ancestors(upto: top)
+ .ancestors(upto: top, hierarchy_order: hierarchy_order)
end
def self_and_ancestors
@@ -243,7 +243,7 @@ class Namespace < ActiveRecord::Base
end
def root_ancestor
- ancestors.reorder(nil).find_by(parent_id: nil)
+ self_and_ancestors.reorder(nil).find_by(parent_id: nil)
end
def subgroup?
diff --git a/app/models/note.rb b/app/models/note.rb
index 592efb714f3..a6ae4f58ac4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -324,7 +324,7 @@ class Note < ActiveRecord::Base
end
def to_ability_name
- for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
+ for_snippet? ? noteable.class.name.underscore : noteable_type.underscore
end
def can_be_discussion_note?
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 1600acfc575..e82eaf4e069 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -5,7 +5,7 @@ class NotificationSetting < ActiveRecord::Base
ignore_column :events
- enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
+ enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 8ef74539209..bad0e30ceb5 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -1,22 +1,20 @@
# frozen_string_literal: true
class PoolRepository < ActiveRecord::Base
- POOL_PREFIX = '@pools'
+ include Shardable
- belongs_to :shard
- validates :shard, presence: true
+ has_many :member_projects, class_name: 'Project'
- # For now, only pool repositories are tracked in the database. However, we may
- # want to add other repository types in the future
- self.table_name = 'repositories'
+ after_create :correct_disk_path
- has_many :pool_member_projects, class_name: 'Project', foreign_key: :pool_repository_id
+ private
- def shard_name
- shard&.name
+ def correct_disk_path
+ update!(disk_path: storage.disk_path)
end
- def shard_name=(name)
- self.shard = Shard.by_name(name)
+ def storage
+ Storage::HashedProject
+ .new(self, prefix: Storage::HashedProject::POOL_PATH_PREFIX)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 39978d8a4c4..9e736a3b03c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -30,6 +30,7 @@ class Project < ActiveRecord::Base
include FeatureGate
include OptionallySearch
include FromUnion
+ include IgnorableColumn
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -55,6 +56,8 @@ class Project < ActiveRecord::Base
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
+ ignore_column :import_status, :import_jid, :import_error
+
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
@@ -63,6 +66,12 @@ class Project < ActiveRecord::Base
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
+ delegate :scheduled?, :started?, :in_progress?,
+ :failed?, :finished?,
+ prefix: :import, to: :import_state, allow_nil: true
+
+ delegate :no_import?, to: :import_state, allow_nil: true
+
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :resolve_outdated_diff_discussions, false
@@ -76,7 +85,7 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- add_authentication_token_field :runners_token
+ add_authentication_token_field :runners_token, encrypted: true, migrating: true
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
@@ -177,6 +186,7 @@ class Project < ActiveRecord::Base
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :project_repository, inverse_of: :project
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -228,6 +238,7 @@ class Project < ActiveRecord::Base
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
+ has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
has_many :prometheus_metrics
@@ -237,7 +248,17 @@ class Project < ActiveRecord::Base
has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :commit_statuses
- has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
+ # The relation :all_pipelines is intented to be used when we want to get the
+ # whole list of pipelines associated to the project
+ has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
+ # The relation :ci_pipelines is intented to be used when we want to get only
+ # those pipeline which are directly related to CI. There are
+ # other pipelines, like webide ones, that we won't retrieve
+ # if we use this relation.
+ has_many :ci_pipelines,
+ -> { Feature.enabled?(:pipeline_ci_sources_only, default_enabled: true) ? ci_sources : all },
+ class_name: 'Ci::Pipeline',
+ inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and
@@ -280,6 +301,8 @@ class Project < ActiveRecord::Base
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
delegate :add_master, to: :team # @deprecated
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
+ delegate :group_clusters_enabled?, to: :group, allow_nil: true
+ delegate :root_ancestor, to: :namespace, allow_nil: true
# Validations
validates :creator, presence: true, on: :create
@@ -316,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) }
@@ -372,9 +396,16 @@ class Project < ActiveRecord::Base
.where(project_ci_cd_settings: { group_runners_enabled: true })
end
+ scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
+ subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id')
+
+ where('NOT EXISTS (?)', subquery)
+ end
+
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
- chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
+ chronic_duration_attr :build_timeout_human_readable, :build_timeout,
+ default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted'
validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 10.minutes,
@@ -382,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.
@@ -451,8 +485,8 @@ class Project < ActiveRecord::Base
scope :excluding_project, ->(project) { where.not(id: project) }
- scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
- scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
+ # We require an alias to the project_mirror_data_table in order to use import_state in our queries
+ scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
scope :for_group, -> (group) { where(group: group) }
class << self
@@ -535,11 +569,13 @@ class Project < ActiveRecord::Base
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
- def ancestors_upto(top = nil)
+ def ancestors_upto(top = nil, hierarchy_order: nil)
Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
- .base_and_ancestors(upto: top)
+ .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?
@@ -610,7 +646,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
- latest_pipeline = pipelines.latest_successful_for(ref)
+ latest_pipeline = ci_pipelines.latest_successful_for(ref)
if latest_pipeline
latest_pipeline.builds.latest.with_artifacts_archive
@@ -628,6 +664,14 @@ class Project < ActiveRecord::Base
id && persisted?
end
+ def import_status
+ import_state&.status || 'none'
+ end
+
+ def human_import_status_name
+ import_state&.human_status_name || 'none'
+ end
+
def add_import_job
job_id =
if forked?
@@ -659,7 +703,7 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id)
end
- update(import_error: nil)
+ import_state.update(last_error: nil)
remove_import_data
end
@@ -721,130 +765,6 @@ class Project < ActiveRecord::Base
import_url.present?
end
- def imported?
- import_finished?
- end
-
- def import_in_progress?
- import_started? || import_scheduled?
- end
-
- def import_state_args
- {
- status: self[:import_status],
- jid: self[:import_jid],
- last_error: self[:import_error]
- }
- end
-
- def ensure_import_state(force: false)
- return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
- return unless import_state.nil?
-
- if persisted?
- create_import_state(import_state_args)
-
- update_column(:import_status, 'none')
- else
- build_import_state(import_state_args)
-
- self[:import_status] = 'none'
- end
- end
-
- def human_import_status_name
- ensure_import_state
-
- import_state.human_status_name
- end
-
- def import_schedule
- ensure_import_state(force: true)
-
- import_state.schedule
- end
-
- def force_import_start
- ensure_import_state(force: true)
-
- import_state.force_start
- end
-
- def import_start
- ensure_import_state(force: true)
-
- import_state.start
- end
-
- def import_fail
- ensure_import_state(force: true)
-
- import_state.fail_op
- end
-
- def import_finish
- ensure_import_state(force: true)
-
- import_state.finish
- end
-
- def import_jid=(new_jid)
- ensure_import_state(force: true)
-
- import_state.jid = new_jid
- end
-
- def import_jid
- ensure_import_state
-
- import_state&.jid
- end
-
- def import_error=(new_error)
- ensure_import_state(force: true)
-
- import_state.last_error = new_error
- end
-
- def import_error
- ensure_import_state
-
- import_state&.last_error
- end
-
- def import_status=(new_status)
- ensure_import_state(force: true)
-
- import_state.status = new_status
- end
-
- def import_status
- ensure_import_state
-
- import_state&.status || 'none'
- end
-
- def no_import?
- import_status == 'none'
- end
-
- def import_started?
- # import? does SQL work so only run it if it looks like there's an import running
- import_status == 'started' && import?
- end
-
- def import_scheduled?
- import_status == 'scheduled'
- end
-
- def import_failed?
- import_status == 'failed'
- end
-
- def import_finished?
- import_status == 'finished'
- end
-
def safe_import_url
Gitlab::UrlSanitizer.new(import_url).masked_url
end
@@ -985,9 +905,9 @@ class Project < ActiveRecord::Base
end
def readme_url
- readme = repository.readme
- if readme
- Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path))
+ readme_path = repository.readme_path
+ if readme_path
+ Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme_path))
end
end
@@ -1166,6 +1086,12 @@ class Project < ActiveRecord::Base
path
end
+ def all_clusters
+ group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )
+
+ Clusters::Cluster.from_union([clusters, group_clusters])
+ end
+
def items_for(entity)
case entity
when 'issue' then
@@ -1246,6 +1172,11 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
+ # Is overriden in EE
+ def lfs_http_url_to_repo(_)
+ http_url_to_repo
+ end
+
def forked?
fork_network && fork_network.root_project != self
end
@@ -1313,6 +1244,13 @@ class Project < ActiveRecord::Base
false
end
+ def track_project_repository
+ return unless hashed_storage?(:repository)
+
+ project_repo = project_repository || build_project_repository
+ project_repo.update!(shard_name: repository_storage, disk_path: disk_path)
+ end
+
def create_repository(force: false)
# Forked import is handled asynchronously
return if forked? && !force
@@ -1469,7 +1407,7 @@ class Project < ActiveRecord::Base
return unless sha
- pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
+ ci_pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
def latest_successful_pipeline_for_default_branch
@@ -1478,12 +1416,12 @@ class Project < ActiveRecord::Base
end
@latest_successful_pipeline_for_default_branch =
- pipelines.latest_successful_for(default_branch)
+ ci_pipelines.latest_successful_for(default_branch)
end
def latest_successful_pipeline_for(ref = nil)
if ref && ref != default_branch
- pipelines.latest_successful_for(ref)
+ ci_pipelines.latest_successful_for(ref)
else
latest_successful_pipeline_for_default_branch
end
@@ -1643,8 +1581,8 @@ class Project < ActiveRecord::Base
def after_import
repository.after_import
wiki.repository.after_import
- import_finish
- remove_import_jid
+ import_state.finish
+ import_state.remove_jid
update_project_counter_caches
after_create_default_branch
refresh_markdown_cache!
@@ -1684,32 +1622,11 @@ class Project < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
- def remove_import_jid
- return unless import_jid
-
- Gitlab::SidekiqStatus.unset(import_jid)
-
- import_state.update_column(:jid, nil)
- end
-
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
- def mark_import_as_failed(error_message)
- original_errors = errors.dup
- sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
-
- import_fail
-
- import_state.update_column(:last_error, sanitized_message)
- rescue ActiveRecord::ActiveRecordError => e
- Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
- ensure
- @errors = original_errors
- end
-
def add_export_job(current_user:, after_export_strategy: nil, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
@@ -1986,17 +1903,6 @@ class Project < ActiveRecord::Base
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
end
- # Refreshes the expiration time of the associated import job ID.
- #
- # This method can be used by asynchronous importers to refresh the status,
- # preventing the StuckImportJobsWorker from marking the import as failed.
- def refresh_import_jid_expiration
- return unless import_jid
-
- Gitlab::SidekiqStatus
- .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
- end
-
def badges
return project_badges unless group
@@ -2071,6 +1977,10 @@ 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
+
private
def use_hashed_storage
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index 7126bb66d80..488f0cb5971 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -69,4 +69,33 @@ class ProjectImportState < ActiveRecord::Base
ensure
@errors = original_errors
end
+
+ alias_method :no_import?, :none?
+
+ def in_progress?
+ scheduled? || started?
+ end
+
+ def started?
+ # import? does SQL work so only run it if it looks like there's an import running
+ status == 'started' && project.import?
+ end
+
+ def remove_jid
+ return unless jid
+
+ Gitlab::SidekiqStatus.unset(jid)
+
+ update_column(:jid, nil)
+ end
+
+ # Refreshes the expiration time of the associated import job ID.
+ #
+ # This method can be used by asynchronous importers to refresh the status,
+ # preventing the StuckImportJobsWorker from marking the import as failed.
+ def refresh_jid_expiration
+ return unless jid
+
+ Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
end
diff --git a/app/models/project_repository.rb b/app/models/project_repository.rb
new file mode 100644
index 00000000000..38913f3f2f5
--- /dev/null
+++ b/app/models/project_repository.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectRepository < ActiveRecord::Base
+ include Shardable
+
+ belongs_to :project, inverse_of: :project_repository
+
+ class << self
+ def find_project(disk_path)
+ find_by(disk_path: disk_path)&.project
+ end
+ end
+end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index c52a531e5fe..b801fd84a07 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -110,14 +110,12 @@ class KubernetesService < DeploymentService
# Clusters::Platforms::Kubernetes, it won't be used on this method
# as it's only needed for Clusters::Cluster.
def predefined_variables(project:)
- config = YAML.dump(kubeconfig)
-
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false)
.append(key: 'KUBE_NAMESPACE', value: actual_namespace)
- .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true)
if ca_pem.present?
variables
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 6f39a5e6e83..d60a6a7efa3 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -38,11 +38,11 @@ class PipelinesEmailService < Service
end
def can_test?
- project.pipelines.any?
+ project.ci_pipelines.any?
end
def test_data(project, user)
- data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+ data = Gitlab::DataBuilder::Pipeline.build(project.ci_pipelines.last)
data[:user] = user.hook_attrs
data
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 211e5c3fcbf..60cb2d380d5 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -71,7 +71,7 @@ class PrometheusService < MonitoringService
end
def prometheus_client
- RestClient::Resource.new(api_url) if api_url && manual_configuration? && active?
+ RestClient::Resource.new(api_url, max_redirects: 0) if api_url && manual_configuration? && active?
end
def prometheus_available?
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index a3415a4a14c..b7b4d0f1be9 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -15,8 +15,6 @@ class RemoteMirror < ActiveRecord::Base
insecure_mode: true,
algorithm: 'aes-256-cbc'
- default_value_for :only_protected_branches, true
-
belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 427dac99b79..35dd120856d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -35,7 +35,7 @@ class Repository
#
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
- CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
+ CACHED_METHODS = %i(size commit_count rendered_readme readme_path contribution_guide
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
@@ -48,7 +48,7 @@ class Repository
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
- readme: :rendered_readme,
+ readme: %i(rendered_readme readme_path),
changelog: :changelog,
license: %i(license_blob license_key license),
contributing: :contribution_guide,
@@ -591,6 +591,11 @@ class Repository
head_tree&.readme
end
+ def readme_path
+ readme&.path
+ end
+ cache_method :readme_path
+
def rendered_readme
return unless readme
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/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index 90710f73fd3..911fb7e9ce9 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -5,17 +5,19 @@ module Storage
attr_accessor :project
delegate :gitlab_shell, :repository_storage, to: :project
- ROOT_PATH_PREFIX = '@hashed'.freeze
+ REPOSITORY_PATH_PREFIX = '@hashed'
+ POOL_PATH_PREFIX = '@pools'
- def initialize(project)
+ def initialize(project, prefix: REPOSITORY_PATH_PREFIX)
@project = project
+ @prefix = prefix
end
# Base directory
#
# @return [String] directory where repository is stored
def base_dir
- "#{ROOT_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
+ "#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
end
# Disk path is used to build repository and project's wiki path on disk
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/models/wiki_page.rb b/app/models/wiki_page.rb
index 7769c3d71c0..b1d6d461928 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -85,6 +85,12 @@ class WikiPage
alias_method :to_param, :slug
+ def human_title
+ return 'Home' if title == 'home'
+
+ title
+ end
+
# The formatted title of this page.
def title
if @attributes[:title]
diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb
index 67e9bc12804..4d4f0ba9267 100644
--- a/app/policies/commit_policy.rb
+++ b/app/policies/commit_policy.rb
@@ -2,4 +2,6 @@
class CommitPolicy < BasePolicy
delegate { @subject.project }
+
+ rule { can?(:download_code) }.enable :read_commit
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index bbc2b48b856..f22843b6463 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -9,8 +9,17 @@ class NotePolicy < BasePolicy
condition(:editable, scope: :subject) { @subject.editable? }
+ condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") }
+
rule { ~editable }.prevent :admin_note
+ # If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes
+ rule { ~can_read_noteable }.policy do
+ prevent :read_note
+ prevent :admin_note
+ prevent :resolve_note
+ end
+
rule { is_author }.policy do
enable :read_note
enable :admin_note
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/README.md b/app/serializers/README.md
index 0337f88db5f..bb94745b0b5 100644
--- a/app/serializers/README.md
+++ b/app/serializers/README.md
@@ -180,7 +180,7 @@ def index
render json: MyResourceSerializer
.new(current_user: @current_user)
.represent_details(@project.resources)
- nd
+ end
end
```
@@ -196,7 +196,7 @@ def index
.represent_details(@project.resources),
count: @project.resources.count
}
- nd
+ end
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/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
new file mode 100644
index 00000000000..58ab804a3c8
--- /dev/null
+++ b/app/serializers/issue_board_entity.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class IssueBoardEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :iid
+ expose :title
+
+ expose :confidential
+ expose :due_date
+ expose :project_id
+ expose :relative_position
+
+ expose :project do |issue|
+ API::Entities::Project.represent issue.project, only: [:id, :path]
+ end
+
+ expose :milestone, expose_nil: false do |issue|
+ API::Entities::Project.represent issue.milestone, only: [:id, :title]
+ end
+
+ expose :assignees do |issue|
+ API::Entities::UserBasic.represent issue.assignees, only: [:id, :name, :username, :avatar_url]
+ end
+
+ expose :labels do |issue|
+ LabelEntity.represent issue.labels, project: issue.project, only: [:id, :title, :description, :color, :priority, :text_color]
+ end
+
+ expose :reference_path, if: -> (issue) { issue.project } do |issue, options|
+ options[:include_full_project_path] ? issue.to_reference(full: true) : issue.to_reference
+ end
+
+ expose :real_path, if: -> (issue) { issue.project } do |issue|
+ project_issue_path(issue.project, issue)
+ end
+
+ expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
+ project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar')
+ end
+
+ expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
+ toggle_subscription_project_issue_path(issue.project, issue)
+ end
+
+ expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
+ project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
+ end
+end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index 37cf5e28396..d66f0a5acb7 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -4,15 +4,17 @@ class IssueSerializer < BaseSerializer
# This overrided method takes care of which entity should be used
# to serialize the `issue` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
- def represent(merge_request, opts = {})
+ def represent(issue, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
IssueSidebarEntity
+ when 'board'
+ IssueBoardEntity
else
IssueEntity
end
- super(merge_request, opts, entity)
+ super(issue, opts, entity)
end
end
diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb
index 98743d62b50..5082245dda9 100644
--- a/app/serializers/label_entity.rb
+++ b/app/serializers/label_entity.rb
@@ -12,4 +12,8 @@ class LabelEntity < Grape::Entity
expose :text_color
expose :created_at
expose :updated_at
+
+ expose :priority, if: -> (*) { options.key?(:project) } do |label|
+ label.priority(options[:project])
+ end
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index aef838409e0..c9669e59199 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -23,6 +23,7 @@ class PipelineEntity < Grape::Entity
expose :latest?, as: :latest
expose :stuck?, as: :stuck
expose :auto_devops_source?, as: :auto_devops
+ expose :merge_request?, as: :merge_request
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
@@ -48,6 +49,7 @@ class PipelineEntity < Grape::Entity
expose :tag?, as: :tag
expose :branch?, as: :branch
+ expose :merge_request?, as: :merge_request
end
expose :commit, using: CommitEntity
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/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index 2a337918d21..40aa9250885 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -6,6 +6,7 @@ class AccessTokenValidationService
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
+ IMPERSONATION_DISABLED = :impersonation_disabled
attr_reader :token, :request
@@ -24,6 +25,11 @@ class AccessTokenValidationService
elsif !self.include_any_scope?(scopes)
return INSUFFICIENT_SCOPE
+ elsif token.respond_to?(:impersonation) &&
+ token.impersonation &&
+ !Gitlab.config.gitlab.impersonation_enabled
+ return IMPERSONATION_DISABLED
+
else
return VALID
end
diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb
new file mode 100644
index 00000000000..a1dd00721b5
--- /dev/null
+++ b/app/services/ci/archive_trace_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Ci
+ class ArchiveTraceService
+ def execute(job)
+ job.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
+ # It's already archived, thus we can safely ignore this exception.
+ rescue => e
+ # Tracks this error with application logs, Sentry, and Prometheus.
+ # If `archive!` keeps failing for over a week, that could incur data loss.
+ # (See more https://docs.gitlab.com/ee/administration/job_traces.html#new-live-trace-architecture)
+ # In order to avoid interrupting the system, we do not raise an exception here.
+ archive_error(e, job)
+ end
+
+ private
+
+ def failed_archive_counter
+ @failed_archive_counter ||=
+ Gitlab::Metrics.counter(:job_trace_archive_failed_total,
+ "Counter of failed attempts of trace archiving")
+ end
+
+ def archive_error(error, job)
+ failed_archive_counter.increment
+ Rails.logger.error "Failed to archive trace. id: #{job.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502',
+ extra: { job_id: job.id })
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 92a8438ab2f..19b5552887f 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -4,6 +4,8 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline
+ CreateError = Class.new(StandardError)
+
SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
@@ -12,7 +14,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -23,6 +25,7 @@ module Ci
before_sha: params[:before],
trigger_request: trigger_request,
schedule: schedule,
+ merge_request: merge_request,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
@@ -47,6 +50,14 @@ module Ci
pipeline
end
+ def execute!(*args, &block)
+ execute(*args, &block).tap do |pipeline|
+ unless pipeline.persisted?
+ raise CreateError, pipeline.errors.full_messages.join(',')
+ end
+ end
+ end
+
private
def commit
@@ -67,7 +78,7 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def auto_cancelable_pipelines
- project.pipelines
+ project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
.where.not(sha: project.commit(pipeline.ref).try(:id))
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 3df43657fa0..e029323774c 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -12,7 +12,8 @@ module Clusters
create_gitlab_service_account!
configure_kubernetes
cluster.save!
- configure_project_service_account
+
+ ClusterPlatformConfigureWorker.perform_async(cluster.id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
@@ -25,7 +26,7 @@ module Clusters
private
def create_gitlab_service_account!
- Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator(
+ Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator(
kube_client,
rbac: create_rbac_cluster?
).execute
@@ -55,15 +56,6 @@ module Clusters
).execute
end
- def configure_project_service_account
- kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
-
- Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
- cluster: cluster,
- kubernetes_namespace: kubernetes_namespace
- ).execute
- end
-
def authorization_type
create_rbac_cluster? ? 'rbac' : 'abac'
end
diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
index b31426556f6..806f320381d 100644
--- a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
+++ b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
@@ -27,7 +27,7 @@ module Clusters
end
def create_project_service_account
- Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator(
+ Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService.namespace_creator(
platform.kubeclient,
service_account_name: kubernetes_namespace.service_account_name,
service_account_namespace: kubernetes_namespace.namespace,
diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb
index dfc4bf7a358..49e766cbf13 100644
--- a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
+++ b/app/services/clusters/gcp/kubernetes/create_or_update_service_account_service.rb
@@ -3,7 +3,7 @@
module Clusters
module Gcp
module Kubernetes
- class CreateServiceAccountService
+ class CreateOrUpdateServiceAccountService
def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient
@service_account_name = service_account_name
@@ -38,8 +38,9 @@ module Clusters
def execute
ensure_project_namespace_exists if namespace_creator
- kubeclient.create_service_account(service_account_resource)
- kubeclient.create_secret(service_account_token_resource)
+
+ kubeclient.create_or_update_service_account(service_account_resource)
+ kubeclient.create_or_update_secret(service_account_token_resource)
create_role_or_cluster_role_binding if rbac
end
@@ -56,9 +57,9 @@ module Clusters
def create_role_or_cluster_role_binding
if namespace_creator
- kubeclient.create_role_binding(role_binding_resource)
+ kubeclient.create_or_update_role_binding(role_binding_resource)
else
- kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
+ kubeclient.create_or_update_cluster_role_binding(cluster_role_binding_resource)
end
end
diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb
new file mode 100644
index 00000000000..7c82b98a33f
--- /dev/null
+++ b/app/services/clusters/refresh_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Clusters
+ class RefreshService
+ def self.create_or_update_namespaces_for_cluster(cluster)
+ projects_with_missing_kubernetes_namespaces_for_cluster(cluster).each do |project|
+ create_or_update_namespace(cluster, project)
+ end
+ end
+
+ def self.create_or_update_namespaces_for_project(project)
+ clusters_with_missing_kubernetes_namespaces_for_project(project).each do |cluster|
+ create_or_update_namespace(cluster, project)
+ end
+ end
+
+ def self.projects_with_missing_kubernetes_namespaces_for_cluster(cluster)
+ cluster.all_projects.missing_kubernetes_namespace(cluster.kubernetes_namespaces)
+ end
+
+ private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster
+
+ def self.clusters_with_missing_kubernetes_namespaces_for_project(project)
+ project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ end
+
+ private_class_method :clusters_with_missing_kubernetes_namespaces_for_project
+
+ def self.create_or_update_namespace(cluster, project)
+ kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace_for_project(project)
+
+ ::Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
+ end
+
+ private_class_method :create_or_update_namespace
+ end
+end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index c9d3ee31d82..927634c2159 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -8,6 +8,7 @@ module Files
transformer = Lfs::FileTransformer.new(project, @branch_name)
actions = actions_after_lfs_transformation(transformer, params[:actions])
+ actions = transform_move_actions(actions)
commit_actions!(actions)
end
@@ -26,6 +27,16 @@ module Files
end
end
+ # When moving a file, `content: nil` means "use the contents of the previous
+ # file", while `content: ''` means "move the file and set it to empty"
+ def transform_move_actions(actions)
+ actions.map do |action|
+ action[:infer_content] = true if action[:content].nil?
+
+ action
+ end
+ end
+
def commit_actions!(actions)
repository.multi_action(
current_user,
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 28c3219b37b..fe19abf50f6 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -54,6 +54,24 @@ module MergeRequests
merge_request, merge_request.project, current_user, merge_request.assignee)
end
+ def create_merge_request_pipeline(merge_request, user)
+ return unless Feature.enabled?(:ci_merge_request_pipeline,
+ merge_request.source_project,
+ default_enabled: true)
+
+ ##
+ # UpdateMergeRequestsWorker could be retried by an exception.
+ # MR pipelines should not be recreated in such case.
+ return if merge_request.merge_request_pipeline_exists?
+
+ Ci::CreatePipelineService
+ .new(merge_request.source_project, user, ref: merge_request.source_branch)
+ .execute(:merge_request,
+ ignore_skip_ci: true,
+ save_on_errors: false,
+ merge_request: merge_request)
+ end
+
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_for(source_branch, mr_states: [:opened])
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 6081a7d1de0..7bb9fa60515 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -25,6 +25,7 @@ module MergeRequests
def after_create(issuable)
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
+ create_merge_request_pipeline(issuable, current_user)
update_merge_requests_head_pipeline(issuable)
super
@@ -49,18 +50,14 @@ module MergeRequests
merge_request.update(head_pipeline_id: pipeline.id) if pipeline
end
- # rubocop: disable CodeReuse/ActiveRecord
def head_pipeline_for(merge_request)
return unless merge_request.source_project
sha = merge_request.source_branch_sha
return unless sha
- pipelines = merge_request.source_project.pipelines.where(ref: merge_request.source_branch, sha: sha)
-
- pipelines.order(id: :desc).first
+ merge_request.all_pipelines(shas: sha).first
end
- # rubocop: enable CodeReuse/ActiveRecord
def set_projects!
# @project is used to determine whether the user can set the merge request's
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 5fe48da1cd6..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)
@@ -92,6 +106,7 @@ module MergeRequests
end
merge_request.mark_as_unchecked
+ create_merge_request_pipeline(merge_request, current_user)
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 5904bfbf88d..e24ef7f9c87 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -466,6 +466,14 @@ 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
+
protected
def new_resource_email(target, method)
diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb
index 1b578a3c5ce..6608b3da1a8 100644
--- a/app/services/projects/auto_devops/disable_service.rb
+++ b/app/services/projects/auto_devops/disable_service.rb
@@ -34,7 +34,7 @@ module Projects
end
def auto_devops_pipelines
- @auto_devops_pipelines ||= project.pipelines.auto_devops_source
+ @auto_devops_pipelines ||= project.ci_pipelines.auto_devops_source
end
end
end
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/create_service.rb b/app/services/projects/create_service.rb
index 20bfe5af7a1..d03137b63b2 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -9,7 +9,7 @@ module Projects
end
def execute
- if @params[:template_name]&.present?
+ if @params[:template_name].present?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
@@ -86,6 +86,8 @@ module Projects
@project.create_wiki unless skip_wiki?
end
+ @project.track_project_repository
+
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
@@ -94,6 +96,8 @@ module Projects
current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme
+
+ configure_group_clusters_for_project
end
# Refresh the current user's authorizations inline (so they can access the
@@ -119,6 +123,10 @@ module Projects
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
+ def configure_group_clusters_for_project
+ ClusterProjectConfigureWorker.perform_async(@project.id)
+ end
+
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
@@ -148,7 +156,7 @@ module Projects
Rails.logger.error(log_message)
if @project
- @project.mark_import_as_failed(message) if @project.persisted? && @project.import?
+ @project.import_state.mark_as_failed(message) if @project.persisted? && @project.import?
end
@project
@@ -181,7 +189,7 @@ module Projects
def import_schedule
if @project.errors.empty?
- @project.import_schedule if @project.import? && !@project.bare_repository_import?
+ @project.import_state.schedule if @project.import? && !@project.bare_repository_import?
else
fail(error: @project.errors.full_messages.join(', '))
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 4462d504071..f3e026ba38c 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -30,6 +30,7 @@ module Projects
if result
project.write_repository_config
+ project.track_project_repository
else
rollback_folder_move
project.storage_version = nil
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 9d40ab166ff..9db3fd9cf17 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -54,6 +54,7 @@ module Projects
end
attempt_transfer_transaction
+ configure_group_clusters_for_project
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -162,5 +163,9 @@ module Projects
@new_namespace.full_path
)
end
+
+ def configure_group_clusters_for_project
+ ClusterProjectConfigureWorker.perform_async(project.id)
+ end
end
end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 45e0e61e5c4..7e14ddcd017 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -49,7 +49,7 @@ module TestHooks
end
def pipeline_events_data
- pipeline = project.pipelines.first
+ pipeline = project.ci_pipelines.first
throw(:validation_error, 'Ensure the project has CI pipelines.') unless pipeline.present?
Gitlab::DataBuilder::Pipeline.build(pipeline)
diff --git a/app/validators/duration_validator.rb b/app/validators/duration_validator.rb
index 811828169ca..defd28d7d3b 100644
--- a/app/validators/duration_validator.rb
+++ b/app/validators/duration_validator.rb
@@ -14,6 +14,10 @@ class DurationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
- record.errors.add(attribute, "is not a correct duration")
+ if options[:message]
+ record.errors.add(:base, options[:message])
+ else
+ record.errors.add(attribute, "is not a correct duration")
+ end
end
end
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml
index b201e6bf10e..19c2a50ebd9 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/admin/runners/_sort_dropdown.html.haml
@@ -1,7 +1,7 @@
- sorted_by = sort_options_hash[@sort]
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
+ %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
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index bfbc16d37a0..a733f420d11 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -8,7 +8,7 @@
%span.cred (Admin)
.float-right
- - if @user != current_user && @user.can?(:log_in)
+ - if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index f910e90d6ca..600120c4f05 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -9,28 +9,20 @@
.search-holder
.search-field-holder
= search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+ - if @sort.present?
+ = hidden_field_tag :sort, @sort
= icon("search", class: "search-icon")
- .dropdown
- - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
+ = button_tag 'Search users' if Rails.env.test?
+ .dropdown.user-sort-dropdown
+ - toggle_text = if @sort.present? then users_sort_options_hash[@sort] else sort_title_name end
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
Sort by
%li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- = sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
+ - users_sort_options_hash.each do |value, title|
+ = link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do
+ = title
= link_to 'New user', new_admin_user_path, class: 'btn btn-success btn-search'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
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/devise/mailer/email_changed.html.haml b/app/views/devise/mailer/email_changed.html.haml
new file mode 100644
index 00000000000..5398430fdfd
--- /dev/null
+++ b/app/views/devise/mailer/email_changed.html.haml
@@ -0,0 +1,12 @@
+= email_default_heading("Hello, #{@resource.name}!")
+
+- if @resource.try(:unconfirmed_email?)
+ %p
+ We're contacting you to notify you that your email is being changed to #{@resource.reload.unconfirmed_email}.
+- else
+ %p
+ We're contacting you to notify you that your email has been changed to #{@resource.email}.
+
+%p
+ If you did not initiate this change, please contact your administrator
+ immediately.
diff --git a/app/views/devise/mailer/email_changed.text.erb b/app/views/devise/mailer/email_changed.text.erb
new file mode 100644
index 00000000000..18137389e7b
--- /dev/null
+++ b/app/views/devise/mailer/email_changed.text.erb
@@ -0,0 +1,10 @@
+Hello, <%= @resource.name %>!
+
+<% if @resource.try(:unconfirmed_email?) %>
+We're contacting you to notify you that your email is being changed to <%= @resource.reload.unconfirmed_email %>.
+<% else %>
+We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
+<% end %>
+
+If you did not initiate this change, please contact your administrator
+immediately.
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/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index b694103ccaf..f518205f14c 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,8 +1,8 @@
- if current_user
.dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
- = icon('globe')
- %span.light= _("Visibility:")
+ %button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
+ = icon('globe', class: 'mt-1')
+ %span.light.ml-3= _("Visibility:")
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- 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/edit.html.haml b/app/views/groups/edit.html.haml
index 869c54d89ea..39d0f620283 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -37,6 +37,7 @@
.settings-content
= render 'shared/badges/badge_settings'
+= render_if_exists 'groups/custom_project_templates_setting'
= render_if_exists 'groups/templates_setting', expanded: expanded
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
index 836981fc6fd..586b0f6ebfa 100644
--- a/app/views/groups/labels/edit.html.haml
+++ b/app/views/groups/labels/edit.html.haml
@@ -1,4 +1,6 @@
-- page_title 'Edit', @label.name, 'Labels'
+- add_to_breadcrumbs _("Labels"), group_labels_path(@group)
+- breadcrumb_title _("Edit")
+- page_title "Edit", @label.name, _("Labels")
%h3.page-title
Edit Label
diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml
index 538c353cf2d..bb0b8d2b94d 100644
--- a/app/views/groups/labels/new.html.haml
+++ b/app/views/groups/labels/new.html.haml
@@ -1,5 +1,6 @@
-- breadcrumb_title "Labels"
-- page_title 'New Label'
+- add_to_breadcrumbs _("Labels"), group_labels_path(@group)
+- breadcrumb_title _("New")
+- page_title _("New Label")
%h3.page-title
New Label
diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml
index 5f6d7d209d0..c703d5f7f93 100644
--- a/app/views/groups/milestones/edit.html.haml
+++ b/app/views/groups/milestones/edit.html.haml
@@ -1,7 +1,10 @@
-- page_title "Milestones"
+- breadcrumb_title _("Edit")
+- page_title _("Milestones")
+
- render "header_title"
%h3.page-title
Edit Milestone
+%hr
= render "form"
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index d758e314d41..248cb3b0ba5 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -1,7 +1,12 @@
-- breadcrumb_title "Milestones"
-- page_title "Milestones"
+- @no_container = true
+- add_to_breadcrumbs _("Milestones"), group_milestones_path(@group)
+- breadcrumb_title _("New")
+- page_title _("Milestones"), @milestone.name, _("Milestones")
-%h3.page-title
- New Milestone
+%div{ class: container_class }
+ %h3.page-title
+ New Milestone
-= render "form"
+ %hr
+
+ = render "form"
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/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 3b1b5e55302..2336e1e83f9 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -37,11 +37,12 @@
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
+ - case project.import_status
+ - when 'finished'
%span
%i.fa.fa-check
= _('done')
- - elsif project.import_status == 'started'
+ - when 'started'
%i.fa.fa-spinner.fa-spin
= _('started')
- else
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 56d4f2ba881..ef69197e453 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -38,9 +38,10 @@
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
+ - case project.import_status
+ - when 'finished'
= icon('check', text: 'Done')
- - elsif project.import_status == 'started'
+ - when 'started'
= icon('spin', text: 'started')
- else
= project.human_import_status_name
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index 830d141ebea..eca67582d6f 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -34,11 +34,12 @@
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
+ - case project.import_status
+ - when 'finished'
%span
%i.fa.fa-check
= _("done")
- - elsif project.import_status == 'started'
+ - when 'started'
%i.fa.fa-spinner.fa-spin
= _("started")
- else
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index b7bfbae5edf..a5fa12fe7df 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -30,11 +30,12 @@
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
+ - case project.import_status
+ - when 'finished'
%span
%i.fa.fa-check
= _('done')
- - elsif project.import_status == 'started'
+ - when 'started'
%i.fa.fa-spinner.fa-spin
= _('started')
- else
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 347e2820f94..f322b7a956a 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -39,11 +39,12 @@
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
+ - case project.import_status
+ - when 'finished'
%span
%i.fa.fa-check
= _("done")
- - elsif project.import_status == 'started'
+ - when 'started'
%i.fa.fa-spinner.fa-spin
= _("started")
- else
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index c2bb1216c5f..30ab5781014 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -1,5 +1,5 @@
-- page_title "Invitation"
-%h3.page-title Invitation
+- page_title _("Invitation")
+%h3.page-title= _("Invitation")
%p
You have been invited
@@ -24,14 +24,17 @@
- if is_member
%p
- However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
- Sign in using a different account to accept the invitation.
+ - member_source = @member.source.is_a?(Group) ? _("group") : _("project")
+ = _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
- if @member.invite_email != current_user.email
%p
- Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
+ - mail_to_invite_email = mail_to(@member.invite_email)
+ - mail_to_current_user = mail_to(current_user.email)
+ - link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
+ = _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
- unless is_member
.actions
- = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
- = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
+ = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
+ = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index ab15889a465..b89541a3c9f 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -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/note_snippet_email.html.haml b/app/views/notify/note_project_snippet_email.html.haml
index 5e69f01a486..5e69f01a486 100644
--- a/app/views/notify/note_snippet_email.html.haml
+++ b/app/views/notify/note_project_snippet_email.html.haml
diff --git a/app/views/notify/note_snippet_email.text.erb b/app/views/notify/note_project_snippet_email.text.erb
index 413d9e6e9ac..413d9e6e9ac 100644
--- a/app/views/notify/note_snippet_email.text.erb
+++ b/app/views/notify/note_project_snippet_email.text.erb
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/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 2220b4eee96..e167e094240 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -21,7 +21,7 @@
= link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success'
%hr
-- if button_based_providers.any?
+- if display_providers_on_profile?
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
@@ -46,6 +46,7 @@
- else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
+ = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities]
%hr
- if current_user.can_change_username?
.row.prepend-top-default
@@ -66,7 +67,7 @@
%h4.prepend-top-0.danger-title
= s_('Profiles|Delete account')
.col-lg-8
- - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
+ - if current_user.can_be_removed? && can?(current_user, :destroy_user, current_user)
%p
= s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
@@ -79,10 +80,10 @@
confirm_with_password: ('true' if current_user.confirm_deletion_with_password?),
username: current_user.username } }
- else
- - if @user.solo_owned_groups.present?
+ - if current_user.solo_owned_groups.present?
%p
= s_('Profiles|Your account is currently an owner in these groups:')
- %strong= @user.solo_owned_groups.map(&:name).join(', ')
+ %strong= current_user.solo_owned_groups.map(&:name).join(', ')
%p
= s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.')
- else
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/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
new file mode 100644
index 00000000000..d82a3dd70f9
--- /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
+ %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", 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", 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/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index c6789e32dbe..1a74b120c26 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -8,62 +8,50 @@
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- link = commit_path(project, commit, merge_request: merge_request)
-- cache_key = [project.full_path,
- ref,
- commit.id,
- Gitlab::CurrentSettings.current_application_settings,
- @path.presence,
- current_controller?(:commits),
- merge_request&.iid,
- view_details,
- commit.status(ref),
- I18n.locale].compact
-
-= cache(cache_key, expires_in: 1.day) do
- %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
-
- .avatar-cell.d-none.d-sm-block
- = author_avatar(commit, size: 36, has_tooltip: false)
-
- .commit-detail.flex-list
- .commit-content.qa-commit-content
- - if view_details && merge_request
- = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- - else
- = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
- %span.commit-row-message.d-block.d-sm-none
- &middot;
- = commit.short_id
- - if commit.status(ref)
- .d-block.d-sm-none
- = render_commit_status(commit, ref: ref)
- - if commit.description?
- %button.text-expander.js-toggle-button
- = sprite_icon('ellipsis_h', size: 12)
+%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
+
+ .avatar-cell.d-none.d-sm-block
+ = author_avatar(commit, size: 36, has_tooltip: false)
+
+ .commit-detail.flex-list
+ .commit-content.qa-commit-content
+ - if view_details && merge_request
+ = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
+ - else
+ = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
+ %span.commit-row-message.d-block.d-sm-none
+ &middot;
+ = commit.short_id
+ - if commit.status(ref)
+ .d-block.d-sm-none
+ = render_commit_status(commit, ref: ref)
+ - if commit.description?
+ %button.text-expander.js-toggle-button
+ = sprite_icon('ellipsis_h', size: 12)
- .committer
- - commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
- - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
- #{ commit_text.html_safe }
+ .committer
+ - commit_author_link = commit_author_link(commit, avatar: false, size: 24)
+ - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
+ - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
+ #{ commit_text.html_safe }
- - if commit.description?
- %pre.commit-row-description.js-toggle-content.append-bottom-8
- = preserve(markdown_field(commit, :description))
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content.append-bottom-8
+ = preserve(markdown_field(commit, :description))
- .commit-actions.flex-row.d-none.d-sm-flex
- - if request.xhr?
- = render partial: 'projects/commit/signature', object: commit.signature
- - else
- = render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
+ .commit-actions.flex-row.d-none.d-sm-flex
+ - if request.xhr?
+ = render partial: 'projects/commit/signature', object: commit.signature
+ - else
+ = render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
- .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
+ .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
- .commit-sha-group
- .label.label-monospace
- = commit.short_id
- = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
- = link_to_browse_code(project, commit)
+ .commit-sha-group
+ .label.label-monospace
+ = commit.short_id
+ = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
+ = link_to_browse_code(project, commit)
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..aa690b12eb7 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
+ .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/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 996c7b1b960..82f035f24da 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,6 +1,6 @@
- page_title "Find File", @ref
-.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, @options.merge(format: :json)))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) }
+.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @id || @commit.id)) }
.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index b44ea89510b..c63c34c4ebb 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -9,7 +9,7 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 1c50cfbde85..bd0ab2c19f2 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -10,7 +10,7 @@
.card-body
%pre
:preserve
- #{h(@project.import_error)}
+ #{h(@project.import_state.last_error)}
= form_for @project, url: project_import_path(@project), method: :post do |f|
= render "shared/import_form", f: f
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 3b0c828ccd1..422a3a22f87 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,6 @@
- page_title import_in_progress_title
+- @no_container = true
+- @content_class = "limit-container-width" unless fluid_layout
.save-project-loader
.center
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index b8ee4305142..b9d45e83032 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Labels", project_labels_path(@project)
+- breadcrumb_title "Edit"
- page_title "Edit", @label.name, "Labels"
%div{ class: container_class }
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/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 02f59f30a39..c6739231e36 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
-- breadcrumb_title "Labels"
+- add_to_breadcrumbs "Labels", project_labels_path(@project)
+- breadcrumb_title "New"
- page_title "New Label"
%div{ class: container_class }
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index af3f25c6a30..4006a468792 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,6 +1,9 @@
- @no_container = true
+- breadcrumb_title "Edit"
+- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
- page_title "Edit", @milestone.title, "Milestones"
+
%div{ class: container_class }
%h3.page-title
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index c301f517013..01cc951e8c2 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
-- breadcrumb_title "Milestones"
+- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
+- breadcrumb_title "New"
- page_title "New Milestone"
%div{ class: container_class }
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index 8dc042d87d1..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'
+ = 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 2f9bd5b04b6..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', 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/new.html.haml b/app/views/projects/new.html.haml
index 10e3b01096a..a760d02c4c3 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -50,7 +50,7 @@
.project-template
.form-group
%div
- = render 'project_templates', f: f
+ = render 'project_templates', f: f, project: @project
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- if import_sources_enabled?
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 2575efc0981..0f0114d513c 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -24,6 +24,38 @@
- if @pipeline.queued_duration
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .well-segment
+ .icon-container
+ = sprite_icon('flag')
+ - if @pipeline.latest?
+ %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for this branch") }
+ latest
+ - if @pipeline.has_yaml_errors?
+ %span.js-pipeline-url-yaml.badge.badge-danger.has-tooltip{ title: @pipeline.yaml_errors }
+ yaml invalid
+ - if @pipeline.failure_reason?
+ %span.js-pipeline-url-failure.badge.badge-danger.has-tooltip{ title: @pipeline.failure_reason }
+ error
+ - if @pipeline.auto_devops_source?
+ - popover_title_text = _('This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>').html_safe
+ - popover_content_url = help_page_path('topics/autodevops/index.md')
+ - popover_content_text = _('Learn more about Auto DevOps')
+ %a.js-pipeline-url-autodevops.badge.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body",
+ toggle: "popover",
+ placement: "top",
+ html: "true",
+ trigger: "focus",
+ title: "<div class='autodevops-title'>#{popover_title_text}</div>",
+ content: "<a class='autodevops-link' href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>",
+ } }
+ Auto DevOps
+ - if @pipeline.merge_request?
+ %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "This pipeline is run in a merge request context" }
+ merge request
+ - if @pipeline.stuck?
+ %span.js-pipeline-url-stuck.badge.badge-warning
+ stuck
+
.well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml
index 233c3adba0e..5b4d8927045 100644
--- a/app/views/projects/project_templates/_built_in_templates.html.haml
+++ b/app/views/projects/project_templates/_built_in_templates.html.haml
@@ -9,9 +9,9 @@
.text-muted
= template.description
.controls.d-flex.align-items-center
- %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
+ %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
+ = _("Preview")
+ %label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
- %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
- = _("Preview")
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/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 621b7922072..bb328f5344c 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -29,7 +29,7 @@
= f.label :build_timeout_human_readable, _('Timeout'), class: 'label-bold'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
- = _("Per job. If a job passes this threshold, it will be marked as failed")
+ = _('If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like "1 hour". Values without specification represent seconds.')
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
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/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 37535370940..026bc44a05f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -14,7 +14,7 @@
= search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= tags_sort_options_hash[@sort]
= icon('chevron-down')
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 5e0523f0b96..889a13339fd 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,5 +1,5 @@
.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- .table-holder
+ .table-holder.bordered-box
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" }
%thead
%tr
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 601e3f25852..a89df6adfb3 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -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/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
index 2423ac6abce..769d869bd53 100644
--- a/app/views/projects/wikis/_sidebar_wiki_page.html.haml
+++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
@@ -1,3 +1,3 @@
%li{ class: active_when(params[:id] == wiki_page.slug) }
= link_to project_wiki_path(@project, wiki_page) do
- = wiki_page.title.capitalize
+ = wiki_page.human_title
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 80aa1500d53..26671a7b7d2 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,5 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title _("Edit"), @page.title.capitalize, _("Wiki")
+- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, @page)
+- breadcrumb_title @page.persisted? ? _("Edit") : _("New")
+- page_title @page.persisted? ? _("Edit") : _("New"), @page.human_title, _("Wiki")
= wiki_page_errors(@error)
@@ -10,9 +12,9 @@
.nav-text
%h2.wiki-page-title
- if @page.persisted?
- = link_to @page.title.capitalize, project_wiki_path(@project, @page)
+ = link_to @page.human_title, project_wiki_path(@project, @page)
- else
- = @page.title.capitalize
+ = @page.human_title
%span.light
&middot;
- if @page.persisted?
@@ -28,7 +30,7 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } }
+ #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.human_title } }
= render 'form', uploads_path: wiki_attachment_upload_url
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 969a1677d9a..c5fbeeafa54 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,4 +1,4 @@
-- page_title _("History"), @page.title.capitalize, _("Wiki")
+- page_title _("History"), @page.human_title, _("Wiki")
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -6,7 +6,7 @@
.nav-text
%h2.wiki-page-title
- = link_to @page.title.capitalize, project_wiki_path(@project, @page)
+ = link_to @page.human_title, project_wiki_path(@project, @page)
%span.light
&middot;
= _("History")
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index fbf248c2058..cc38ec12fd8 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title @page.title.capitalize
+- breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug)
-- page_title @page.title.capitalize, _("Wiki")
+- page_title @page.human_title, _("Wiki")
- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
.wiki-page-header.has-sidebar-toggle
@@ -9,7 +9,7 @@
= icon('angle-double-left')
.nav-text
- %h2.wiki-page-title= @page.title.capitalize
+ %h2.wiki-page-title= @page.human_title
%span.wiki-last-edit-by
- if @page.last_version
= (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
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/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index 6c4607b2f16..0d0a3c1aa64 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,6 +1,6 @@
- if show_auto_devops_implicitly_enabled_banner?(project)
.auto-devops-implicitly-enabled-banner.alert.alert-warning
- - more_information_link = link_to _('More information'), 'https://docs.gitlab.com/ee/topics/autodevops/', class: 'alert-link'
+ - more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link'
- auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link }
= auto_devops_message.html_safe
.alert-link-group
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index a6ba3b59365..bd68a3e4c84 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -1,5 +1,5 @@
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
%span.light
- if @sort.present?
= milestone_sort_options_hash[@sort]
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 be6d4f1c32b..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-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/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 2237b93a10b..1ae6d1f5ee3 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -9,7 +9,7 @@
- default_sort_by = sort_value_recently_created
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
= options_hash[default_sort_by]
= icon('chevron-down')
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 6939aba6896..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 }
@@ -95,7 +94,10 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- = _('No Label')
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
@@ -139,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/_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/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
index 8a7d037e15b..d664ef1cc2f 100644
--- a/app/views/shared/labels/_sort_dropdown.html.haml
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
- sort_title = label_sort_options_hash[@sort] || sort_title_name_desc
.dropdown.inline
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= sort_title
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
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/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 3dd2842be4f..ed7fefba56d 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -35,8 +35,8 @@
.col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
- if @project
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- - if @project.group
+ - if can_admin_project_milestones? and milestone.active?
+ - if can_admin_group_milestones?
%button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
disabled: true,
type: 'button',
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/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index daf08d9bb2c..559b5aa9c1e 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -45,7 +45,7 @@
= _('Maximum job timeout')
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
- .form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout')
+ .form-text.text-muted= _('This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like "1 hour". Values without specification represent seconds.')
.form-group.row
= label_tag :tag_list, class: 'col-form-label col-sm-2' do
= _('Tags')
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 0ce13ee7a53..ef8664e6f47 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -3,31 +3,31 @@
.d-none.d-sm-block
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
- Edit
+ = _("Edit")
- if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
- Delete
- = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-success", title: "New snippet" do
- New snippet
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
+ = _("Delete")
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: _("New snippet") do
+ = _("New snippet")
- if @snippet.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to _('Submit as spam'), mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
.d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
+ = _("Options")
= icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
%li
- = link_to new_snippet_path, title: "New snippet" do
- New snippet
+ = link_to new_snippet_path, title: _("New snippet") do
+ = _("New snippet")
- if can?(current_user, :admin_personal_snippet, @snippet)
%li
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do
+ = _("Delete")
- if can?(current_user, :update_personal_snippet, @snippet)
%li
= link_to edit_snippet_path(@snippet) do
- Edit
+ = _("Edit")
- if @snippet.submittable_as_spam_by?(current_user)
%li
- = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
+ = link_to _('Submit as spam'), mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index dfea8b40bd8..69d41f8fe5e 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -5,6 +5,6 @@
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
- .nothing-here-block Nothing here.
+ .nothing-here-block= _("Nothing here.")
= paginate @snippets, theme: 'gitlab'
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index dc4b0fd9ba0..c312226dd6c 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -4,7 +4,7 @@
.nav-links.snippet-scope-menu.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
- All
+ = _("All")
%span.badge.badge-pill
- if include_private
= subject.snippets.count
@@ -14,18 +14,18 @@
- if include_private
%li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
- Private
+ = _("Private")
%span.badge.badge-pill
= subject.snippets.are_private.count
%li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
- Internal
+ = _("Internal")
%span.badge.badge-pill
= subject.snippets.are_internal.count
%li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
- Public
+ = _("Public")
%span.badge.badge-pill
= subject.snippets.are_public.count
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 18ebeb78f87..ebc6c0a2605 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,5 +1,6 @@
-- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
+
%h3.page-title
- Edit Snippet
+ = _("Edit Snippet")
%hr
= render 'shared/snippets/form', url: snippet_path(@snippet)
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 9b4a7dbe68d..4f418e2381f 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -1,13 +1,13 @@
-- page_title "By #{@user.name}", "Snippets"
+- page_title _("By %{user_name}") % { user_name: @user.name }, _("Snippets")
%ol.breadcrumb
%li.breadcrumb-item
= link_to snippets_path do
- Snippets
+ = _("Snippets")
%li.breadcrumb-item
= @user.name
.float-right.d-none.d-sm-block
= link_to user_path(@user) do
- #{@user.name} profile page
+ = _("%{user_name} profile page") % { user_name: @user.name }
= render 'snippets'
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 6bc748d346e..114c777bdc2 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
- @hide_breadcrumbs = true
-- page_title "New Snippet"
+- page_title _("New Snippet")
.page-title-holder
%h1.page-title= _('New Snippet')
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index 220ba2b49e6..01b95145937 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -1,7 +1,7 @@
- if current_user
- if note.emoji_awardable?
.note-actions-item
- = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do
+ = link_to '#', title: _('Add reaction'), class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
@@ -9,7 +9,7 @@
- if note_editable
.note-actions-item
- = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
+ = button_tag title: _('Edit comment'), class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 578327883e5..36b4e00e8d5 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,8 +1,8 @@
- @hide_top_links = true
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
-- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- add_to_breadcrumbs _("Snippets"), dashboard_snippets_path
- breadcrumb_title @snippet.to_reference
-- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
+- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
= render 'shared/snippets/header'
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 938cb579e9f..01acbf8eadd 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -7,7 +7,7 @@
%li
%span.light
%i.fa.fa-clock-o
- = event.created_at.strftime('%-I:%M%P')
+ = event.created_at.to_time.in_time_zone.strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- if event.push?
#{event.action_name} #{event.ref_type}
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c0b410472eb..672c77539af 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
@@ -29,6 +28,7 @@
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_platform_configure
+- gcp_cluster:cluster_project_configure
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -132,3 +132,5 @@
- create_note_diff_file
- delete_diff_files
- detect_repository_languages
+- repository_cleanup
+- delete_stored_files
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index c1283e9b2fc..4a9becf0ca7 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -7,7 +7,7 @@ class ArchiveTraceWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
- job.trace.archive!
+ Ci::ArchiveTraceService.new.execute(job)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 7443aad1380..f65ff239866 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -11,21 +11,9 @@ module Ci
# This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
# More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
- begin
- build.trace.archive!
- rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
- rescue => e
- failed_archive_counter.increment
- Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
- end
+ Ci::ArchiveTraceService.new.execute(build)
end
end
# rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def failed_archive_counter
- @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving")
- end
end
end
diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb
index 8f3689f0166..aa7570caa79 100644
--- a/app/workers/cluster_platform_configure_worker.rb
+++ b/app/workers/cluster_platform_configure_worker.rb
@@ -6,17 +6,7 @@ class ClusterPlatformConfigureWorker
def perform(cluster_id)
Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
- next unless cluster.cluster_project
-
- kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
-
- Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
- cluster: cluster,
- kubernetes_namespace: kubernetes_namespace
- ).execute
+ Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
end
-
- rescue ::Kubeclient::HttpError => err
- Rails.logger.error "Failed to create/update Kubernetes namespace for cluster_id: #{cluster_id} with error: #{err.message}"
end
end
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
new file mode 100644
index 00000000000..497e57c0d0b
--- /dev/null
+++ b/app/workers/cluster_project_configure_worker.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class ClusterProjectConfigureWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ def perform(project_id)
+ project = Project.find(project_id)
+
+ ::Clusters::RefreshService.create_or_update_namespaces_for_project(project)
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index 59e6bc2c97d..e2dee315cde 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -24,7 +24,7 @@ module Gitlab
def find_project(id)
# If the project has been marked as failed we want to bail out
# automatically.
- Project.import_started.find_by(id: id)
+ Project.joins_import_state.where(import_state: { status: :started }).find_by(id: id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
index 22bdf441d6b..2baf768bfd1 100644
--- a/app/workers/concerns/project_import_options.rb
+++ b/app/workers/concerns/project_import_options.rb
@@ -18,7 +18,7 @@ module ProjectImportOptions
"import"
end
- project.mark_import_as_failed("Every #{action} attempt has failed: #{job['error_message']}. Please try again.")
+ project.import_state.mark_as_failed(_("Every %{action} attempt has failed: %{job_error_message}. Please try again.") % { action: action, job_error_message: job['error_message'] })
Sidekiq.logger.warn "Failed #{job['class']} with #{job['args']}: #{job['error_message']}"
end
end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 46a133db2a1..4462bc51a24 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -2,11 +2,11 @@
# Used in EE by mirroring
module ProjectStartImport
- def start(project)
- if project.import_started? && project.import_jid == self.jid
+ def start(import_state)
+ if import_state.started? && import_state.jid == self.jid
return true
end
- project.import_start
+ import_state.start
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/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 2b49860025a..0b3437a8a33 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -31,7 +31,7 @@ module Gitlab
# next_stage - The name of the next stage to start when all jobs have been
# completed.
def perform(project_id, waiters, next_stage)
- return unless (project = find_project(project_id))
+ return unless import_state = find_import_state(project_id)
new_waiters = wait_for_jobs(waiters)
@@ -41,7 +41,7 @@ module Gitlab
# the pressure on Redis. We _only_ do this once all jobs are done so
# we don't get stuck forever if one or more jobs failed to notify the
# JobWaiter.
- project.refresh_import_jid_expiration
+ import_state.refresh_jid_expiration
STAGES.fetch(next_stage.to_sym).perform_async(project_id)
else
@@ -64,11 +64,8 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
- def find_project(id)
- # TODO: Only select the JID
- # This is due to the fact that the JID could be present in either the project record or
- # its associated import_state record
- Project.import_started.find_by(id: id)
+ def find_import_state(project_id)
+ ProjectImportState.select(:jid).with_status(:started).find_by(project_id: project_id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 65473026b4c..76723e4a61f 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -16,12 +16,13 @@ module Gitlab
# project_id - The ID of the project that is being imported.
# check_job_id - The ID of the job for which to check the status.
def perform(project_id, check_job_id)
- return unless (project = find_project(project_id))
+ import_state = find_import_state(project_id)
+ return unless import_state
if SidekiqStatus.running?(check_job_id)
# As long as the repository is being cloned we want to keep refreshing
# the import JID status.
- project.refresh_import_jid_expiration
+ import_state.refresh_jid_expiration
self.class.perform_in_the_future(project_id, check_job_id)
end
@@ -31,11 +32,10 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
- def find_project(id)
- # TODO: Only select the JID
- # This is due to the fact that the JID could be present in either the project record or
- # its associated import_state record
- Project.import_started.find_by(id: id)
+ def find_import_state(project_id)
+ ProjectImportState.select(:jid)
+ .with_status(:started)
+ .find_by(project_id: project_id)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index 5726fbb573d..ccfed2ae187 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -23,7 +23,7 @@ module Gitlab
klass.new(project, client).execute
end
- project.refresh_import_jid_expiration
+ project.import_state.refresh_jid_expiration
ImportPullRequestsWorker.perform_async(project.id)
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index 1c5a7139802..37a7a7f4ba0 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -15,7 +15,7 @@ module Gitlab
.new(project, client)
.execute
- project.refresh_import_jid_expiration
+ project.import_state.refresh_jid_expiration
AdvanceStageWorker.perform_async(
project.id,
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/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 85d1ffe0fa9..ac4e9710f33 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -9,18 +9,36 @@ class PipelineScheduleWorker
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
begin
- pipeline = Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
-
- schedule.deactivate! unless pipeline.persisted?
+ Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
rescue => e
- Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}"
+ error(schedule, e)
ensure
schedule.schedule_next_run!
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def error(schedule, error)
+ failed_creation_counter.increment
+
+ Rails.logger.error "Failed to create a scheduled pipeline. " \
+ "schedule_id: #{schedule.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: schedule.id })
+ end
+
+ def failed_creation_counter
+ @failed_creation_counter ||=
+ Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
+ "Counter of failed attempts of pipeline schedule creation")
+ 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_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 68ec66e8499..7eae07d3f6b 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -12,7 +12,7 @@ class RepositoryForkWorker
source_project = target_project.forked_from_project
unless source_project
- return target_project.mark_import_as_failed('Source project cannot be found.')
+ return target_project.import_state.mark_as_failed(_('Source project cannot be found.'))
end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
@@ -33,7 +33,7 @@ class RepositoryForkWorker
end
def start_fork(project)
- return true if start(project)
+ return true if start(project.import_state)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 82189a3c9f5..59691f48a39 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -34,14 +34,14 @@ class RepositoryImportWorker
attr_reader :project
def start_import
- return true if start(project)
+ return true if start(project.import_state)
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
def fail_import(message)
- project.mark_import_as_failed(message)
+ project.import_state.mark_as_failed(message)
end
def template_import?
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 667a4121131..c8a186ba4ce 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -63,6 +63,6 @@ class StuckImportJobsWorker
# rubocop: enable CodeReuse/ActiveRecord
def error_message
- "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds"
+ _("Import timed out. Import took longer than %{import_jobs_expiration} seconds") % { import_jobs_expiration: IMPORT_JOBS_EXPIRATION }
end
end
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 9ce51662969..e8494ffa002 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -6,10 +6,11 @@ class UpdateHeadPipelineForMergeRequestWorker
queue_namespace :pipeline_processing
- # rubocop: disable CodeReuse/ActiveRecord
def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id)
- pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
+
+ sha = merge_request.diff_head_sha
+ pipeline = merge_request.all_pipelines(shas: sha).first
return unless pipeline && pipeline.latest?
@@ -21,7 +22,6 @@ class UpdateHeadPipelineForMergeRequestWorker
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
- # rubocop: enable CodeReuse/ActiveRecord
def log_error_message_for(merge_request)
Rails.logger.error(
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/1979-redesign-mr-widget-approvals-ce.yml b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml
new file mode 100644
index 00000000000..d05b6054b22
--- /dev/null
+++ b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign of MR header sections (CE)
+merge_request: 23465
+author:
+type: changed
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/33705-merge-request-rebase-api.yml b/changelogs/unreleased/33705-merge-request-rebase-api.yml
new file mode 100644
index 00000000000..322fe31ce87
--- /dev/null
+++ b/changelogs/unreleased/33705-merge-request-rebase-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add a rebase API endpoint for merge requests
+merge_request: 23296
+author:
+type: added
diff --git a/changelogs/unreleased/34758-deployment-cluster.yml b/changelogs/unreleased/34758-deployment-cluster.yml
new file mode 100644
index 00000000000..06374098343
--- /dev/null
+++ b/changelogs/unreleased/34758-deployment-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Use group clusters when deploying (DeploymentPlatform)
+merge_request: 22308
+author:
+type: changed
diff --git a/changelogs/unreleased/38495-calendar-activities-in-timezone.yml b/changelogs/unreleased/38495-calendar-activities-in-timezone.yml
new file mode 100644
index 00000000000..778d637609c
--- /dev/null
+++ b/changelogs/unreleased/38495-calendar-activities-in-timezone.yml
@@ -0,0 +1,5 @@
+---
+title: Show user contributions in correct timezone within user profile
+merge_request: 23419
+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/40385-prohibit_impersonation.yml b/changelogs/unreleased/40385-prohibit_impersonation.yml
new file mode 100644
index 00000000000..dd061b17939
--- /dev/null
+++ b/changelogs/unreleased/40385-prohibit_impersonation.yml
@@ -0,0 +1,5 @@
+---
+title: Add config to prohibit impersonation
+merge_request: 23338
+author:
+type: added
diff --git a/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml
new file mode 100644
index 00000000000..cfc74bef638
--- /dev/null
+++ b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml
@@ -0,0 +1,5 @@
+---
+title: When user clicks linenumber in MR changes, highlight that line
+merge_request:
+author:
+type: fixed
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/50264-add-border-around-the-repository-file-tree.yml b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml
new file mode 100644
index 00000000000..6315c3e7f36
--- /dev/null
+++ b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Add border around the repository file tree
+merge_request: 23018
+author:
+type: changed
diff --git a/changelogs/unreleased/50341-cleanup-useless-project-import-attributes.yml b/changelogs/unreleased/50341-cleanup-useless-project-import-attributes.yml
new file mode 100644
index 00000000000..3893f14e15c
--- /dev/null
+++ b/changelogs/unreleased/50341-cleanup-useless-project-import-attributes.yml
@@ -0,0 +1,6 @@
+---
+title: Removes all the irrelevant code and columns that were migrated from the Project
+ table over to the ProjectImportState table
+merge_request: 21497
+author:
+type: performance
diff --git a/changelogs/unreleased/50626-searching-users-by-the-admin-panel-wipes-query-when-using-sort.yml b/changelogs/unreleased/50626-searching-users-by-the-admin-panel-wipes-query-when-using-sort.yml
new file mode 100644
index 00000000000..c3251fea54d
--- /dev/null
+++ b/changelogs/unreleased/50626-searching-users-by-the-admin-panel-wipes-query-when-using-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Allow search and sort users at same time on admin users page
+merge_request: 23439
+author:
+type: fixed
diff --git a/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml b/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml
new file mode 100644
index 00000000000..1c6c8747197
--- /dev/null
+++ b/changelogs/unreleased/50839-webide-mr-dropdown-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Scope default MR search in WebIDE dropdown to current project
+merge_request: 23400
+author:
+type: changed
diff --git a/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml
new file mode 100644
index 00000000000..dc11ede5c8d
--- /dev/null
+++ b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve status emoji being replaced by avatar on mobile
+merge_request: 23408
+author:
+type: other
diff --git a/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml b/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml
new file mode 100644
index 00000000000..86f91fcb427
--- /dev/null
+++ b/changelogs/unreleased/51061-readme-url-n-1-rpc-call-resolved.yml
@@ -0,0 +1,5 @@
+---
+title: Improves performance of Project#readme_url by caching the README path
+merge_request: 23357
+author:
+type: performance
diff --git a/changelogs/unreleased/51083-fix-move-api.yml b/changelogs/unreleased/51083-fix-move-api.yml
new file mode 100644
index 00000000000..8838f6f267e
--- /dev/null
+++ b/changelogs/unreleased/51083-fix-move-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'Commits API: Preserve file content in move operations if unspecified'
+merge_request: 23387
+author:
+type: fixed
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/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/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/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml b/changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml
new file mode 100644
index 00000000000..9e1ee3ede5e
--- /dev/null
+++ b/changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Any option to label filters
+merge_request: 23111
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml b/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml
new file mode 100644
index 00000000000..b393795f491
--- /dev/null
+++ b/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: Unstar icon button is misaligned'
+merge_request: 23444
+author:
+type: fixed
diff --git a/changelogs/unreleased/53659-use-padded-key-for-gcm-ciphers.yml b/changelogs/unreleased/53659-use-padded-key-for-gcm-ciphers.yml
new file mode 100644
index 00000000000..fe9ac7b3dc7
--- /dev/null
+++ b/changelogs/unreleased/53659-use-padded-key-for-gcm-ciphers.yml
@@ -0,0 +1,5 @@
+---
+title: Fix web hook functionality when the database encryption key is too short
+merge_request: 23573
+author:
+type: fixed
diff --git a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml b/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml
deleted file mode 100644
index 44362a8622e..00000000000
--- a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Correctly handle data-loss scenarios when encrypting columns
-merge_request: 23306
-author:
-type: fixed
diff --git a/changelogs/unreleased/53778-remove-site-statistics.yml b/changelogs/unreleased/53778-remove-site-statistics.yml
deleted file mode 100644
index fe006e43671..00000000000
--- a/changelogs/unreleased/53778-remove-site-statistics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed Site Statistics optimization as it was causing problems
-merge_request: 23314
-author:
-type: removed
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/54048-Line-numbers-are-misaligned-in-file-blame-view.yml b/changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml
new file mode 100644
index 00000000000..8ceac4ec869
--- /dev/null
+++ b/changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml
@@ -0,0 +1,5 @@
+---
+title: Fix line height of numbers in file blame view
+merge_request: 23090
+author: Johann Hubert Sonntagbauer
+type: fixed
diff --git a/changelogs/unreleased/54218-fix-mergeUrlParams.yml b/changelogs/unreleased/54218-fix-mergeUrlParams.yml
new file mode 100644
index 00000000000..dae06b66e8e
--- /dev/null
+++ b/changelogs/unreleased/54218-fix-mergeUrlParams.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix mergeUrlParams with fragment URL"
+merge_request: 54218
+author: Thomas Holder
+type: fixed
diff --git a/changelogs/unreleased/54282-tooltip-stuck.yml b/changelogs/unreleased/54282-tooltip-stuck.yml
deleted file mode 100644
index 655870499a0..00000000000
--- a/changelogs/unreleased/54282-tooltip-stuck.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes stuck tooltip on stop env button
-merge_request: 23244
-author:
-type: fixed
diff --git a/changelogs/unreleased/54336-include-tags-into-pipeline-detail-view.yml b/changelogs/unreleased/54336-include-tags-into-pipeline-detail-view.yml
new file mode 100644
index 00000000000..11f941ab9bb
--- /dev/null
+++ b/changelogs/unreleased/54336-include-tags-into-pipeline-detail-view.yml
@@ -0,0 +1,5 @@
+---
+title: Merge request pipeline tag, and adds tags to pipeline view
+merge_request: 23364
+author:
+type: added
diff --git a/changelogs/unreleased/54571-runner-tags.yml b/changelogs/unreleased/54571-runner-tags.yml
new file mode 100644
index 00000000000..1bb19d22e9c
--- /dev/null
+++ b/changelogs/unreleased/54571-runner-tags.yml
@@ -0,0 +1,5 @@
+---
+title: Adds margins between tags when a job is stuck
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml
new file mode 100644
index 00000000000..671d1590991
--- /dev/null
+++ b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Order By dropdown menu styling in tablet and mobile screens
+merge_request: 23446
+author:
+type: fixed
diff --git a/changelogs/unreleased/54826-use-read_repository-scope-on-read-only-files-endpoints.yml b/changelogs/unreleased/54826-use-read_repository-scope-on-read-only-files-endpoints.yml
new file mode 100644
index 00000000000..ef8e93fca43
--- /dev/null
+++ b/changelogs/unreleased/54826-use-read_repository-scope-on-read-only-files-endpoints.yml
@@ -0,0 +1,5 @@
+---
+title: Use read_repository scope on read-only files API
+merge_request: 23534
+author:
+type: fixed
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/_acet-fix-flash-styling.yml b/changelogs/unreleased/_acet-fix-flash-styling.yml
new file mode 100644
index 00000000000..57354c04899
--- /dev/null
+++ b/changelogs/unreleased/_acet-fix-flash-styling.yml
@@ -0,0 +1,5 @@
+---
+title: Fix flash notice styling for fluid layout
+merge_request: 23382
+author:
+type: fixed
diff --git a/changelogs/unreleased/ab-approximate-counts.yml b/changelogs/unreleased/ab-approximate-counts.yml
new file mode 100644
index 00000000000..8a67239d031
--- /dev/null
+++ b/changelogs/unreleased/ab-approximate-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Approximate counting strategy with TABLESAMPLE.
+merge_request: 22650
+author:
+type: performance
diff --git a/changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml b/changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml
deleted file mode 100644
index 7fb11f24902..00000000000
--- a/changelogs/unreleased/auto-devops-support-for-group-security-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Auto DevOps support for Group Security Dashboard
-merge_request: 23165
-author:
-type: fixed
diff --git a/changelogs/unreleased/bump_gpgme_gem.yml b/changelogs/unreleased/bump_gpgme_gem.yml
new file mode 100644
index 00000000000..4c0067cb824
--- /dev/null
+++ b/changelogs/unreleased/bump_gpgme_gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump gpgme gem version from 2.0.13 to 2.0.18
+merge_request:
+author: asaparov
+type: other
diff --git a/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml
new file mode 100644
index 00000000000..b5fd99c304f
--- /dev/null
+++ b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix API::Namespaces routing to accept namepaces with dots
+merge_request: 22912
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml b/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
deleted file mode 100644
index 6cc743d6f3a..00000000000
--- a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display impersonation token value only after creation
-merge_request: 22916
-author:
-type: fixed
diff --git a/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml b/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml
new file mode 100644
index 00000000000..31c131045b9
--- /dev/null
+++ b/changelogs/unreleased/check-if-fetched-data-does-is-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Validate chunk size when persist
+merge_request: 23341
+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
new file mode 100644
index 00000000000..3e5ecdcf51e
--- /dev/null
+++ b/changelogs/unreleased/define-default-value-for-only-except-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Define the default value for only/except policies
+merge_request: 23531
+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-expand-commit-file.yml b/changelogs/unreleased/diff-expand-commit-file.yml
new file mode 100644
index 00000000000..8ca784d75c1
--- /dev/null
+++ b/changelogs/unreleased/diff-expand-commit-file.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed diff files expanding not loading commit content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/diff-fix-expanding.yml b/changelogs/unreleased/diff-fix-expanding.yml
new file mode 100644
index 00000000000..8ba7f87addc
--- /dev/null
+++ b/changelogs/unreleased/diff-fix-expanding.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed multiple diff line discussions not expanding
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-batch-loader-sidekiq.yml b/changelogs/unreleased/dm-batch-loader-sidekiq.yml
deleted file mode 100644
index 87936dc2603..00000000000
--- a/changelogs/unreleased/dm-batch-loader-sidekiq.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clear BatchLoader context between Sidekiq jobs
-merge_request: 23308
-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/document-raw-snippet-api.yml b/changelogs/unreleased/document-raw-snippet-api.yml
new file mode 100644
index 00000000000..3b8818cea5c
--- /dev/null
+++ b/changelogs/unreleased/document-raw-snippet-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix lack of documentation on how to fetch a snippet's content using API
+merge_request: 23448
+author: Colin Leroy
+type: other
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-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-gb-encrypt-runners-tokens.yml b/changelogs/unreleased/fix-gb-encrypt-runners-tokens.yml
new file mode 100644
index 00000000000..4ce4f96c1dd
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-encrypt-runners-tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Encrypt runners tokens
+merge_request: 23412
+author:
+type: security
diff --git a/changelogs/unreleased/fix-gb-improve-timeout-inputs-help-sections.yml b/changelogs/unreleased/fix-gb-improve-timeout-inputs-help-sections.yml
new file mode 100644
index 00000000000..52b431edf2c
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-improve-timeout-inputs-help-sections.yml
@@ -0,0 +1,5 @@
+---
+title: Improve help and validation sections of maximum build timeout inputs
+merge_request: 23586
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml
new file mode 100644
index 00000000000..ab926fbd43b
--- /dev/null
+++ b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unrelated deployment status in MR widget
+merge_request: 23175
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-multiple-comments-shade-overlap.yml b/changelogs/unreleased/fix-multiple-comments-shade-overlap.yml
new file mode 100644
index 00000000000..20005ba355e
--- /dev/null
+++ b/changelogs/unreleased/fix-multiple-comments-shade-overlap.yml
@@ -0,0 +1,5 @@
+---
+title: Fix multiple commits shade overlapping vertical discussion line
+merge_request: 23515
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-not-render-emoji.yml b/changelogs/unreleased/fix-not-render-emoji.yml
deleted file mode 100644
index 857b97004f0..00000000000
--- a/changelogs/unreleased/fix-not-render-emoji.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix not render emoji in filter dropdown
-merge_request: 23112
-author: Hiroyuki Sato
-type: fixed
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-change-container-width-for-project-import.yml b/changelogs/unreleased/gt-change-container-width-for-project-import.yml
new file mode 100644
index 00000000000..ec2beb15912
--- /dev/null
+++ b/changelogs/unreleased/gt-change-container-width-for-project-import.yml
@@ -0,0 +1,5 @@
+---
+title: Change container width for project import
+merge_request: 23318
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-externalize-app-views-invites.yml b/changelogs/unreleased/gt-externalize-app-views-invites.yml
new file mode 100644
index 00000000000..b5a22177f9b
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-invites.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/invites`
+merge_request: 23205
+author: Tao Wang
+type: other
diff --git a/changelogs/unreleased/gt-externalize-app-views-snippets.yml b/changelogs/unreleased/gt-externalize-app-views-snippets.yml
new file mode 100644
index 00000000000..633aa9f2534
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/snippets`
+merge_request: 23351
+author: Tao Wang
+type: other
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/ignore-failed-pipeline-creation-on-pipeline-schedule.yml b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml
new file mode 100644
index 00000000000..90f47aa12db
--- /dev/null
+++ b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml
@@ -0,0 +1,5 @@
+---
+title: Remove auto deactivation when failed to create a pipeline via pipeline schedules
+merge_request: 22243
+author:
+type: changed
diff --git a/changelogs/unreleased/improve_auto_devops_migration_debug.yml b/changelogs/unreleased/improve_auto_devops_migration_debug.yml
new file mode 100644
index 00000000000..96a78808361
--- /dev/null
+++ b/changelogs/unreleased/improve_auto_devops_migration_debug.yml
@@ -0,0 +1,6 @@
+---
+title: 'Auto DevOps: Add echo for each branch of the deploy() function where we run
+ helm upgrade'
+merge_request: 23499
+author:
+type: changed
diff --git a/changelogs/unreleased/include-new-link-in-breadcrumb.yml b/changelogs/unreleased/include-new-link-in-breadcrumb.yml
new file mode 100644
index 00000000000..68c808d66d7
--- /dev/null
+++ b/changelogs/unreleased/include-new-link-in-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Include new link in breadcrumb for issues, merge requests, milestones, and labels
+merge_request: 18515
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml
new file mode 100644
index 00000000000..d21254b16d0
--- /dev/null
+++ b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml
@@ -0,0 +1,5 @@
+---
+title: Add empty state for graphs with no values
+merge_request: 22630
+author:
+type: fixed
diff --git a/changelogs/unreleased/jupyter-tls.yml b/changelogs/unreleased/jupyter-tls.yml
new file mode 100644
index 00000000000..4111edd34ff
--- /dev/null
+++ b/changelogs/unreleased/jupyter-tls.yml
@@ -0,0 +1,5 @@
+---
+title: "#52753: HTTPS for JupyterHub installation"
+merge_request: 23479
+author: Amit Rathi
+type: added
diff --git a/changelogs/unreleased/legacy_fallback_for_project_clusters_only.yml b/changelogs/unreleased/legacy_fallback_for_project_clusters_only.yml
new file mode 100644
index 00000000000..c8e959176d0
--- /dev/null
+++ b/changelogs/unreleased/legacy_fallback_for_project_clusters_only.yml
@@ -0,0 +1,5 @@
+---
+title: Fallback to admin KUBE_TOKEN for project clusters only
+merge_request: 23527
+author:
+type: other
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/mr-pipelines-2.yml b/changelogs/unreleased/mr-pipelines-2.yml
new file mode 100644
index 00000000000..683c626c3ce
--- /dev/null
+++ b/changelogs/unreleased/mr-pipelines-2.yml
@@ -0,0 +1,5 @@
+---
+title: Merge request pipelines
+merge_request: 23217
+author:
+type: added
diff --git a/changelogs/unreleased/multiple-diff-line-discussions-fix.yml b/changelogs/unreleased/multiple-diff-line-discussions-fix.yml
new file mode 100644
index 00000000000..870a8ab3815
--- /dev/null
+++ b/changelogs/unreleased/multiple-diff-line-discussions-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed duplicate discussions getting added to diff lines
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/non-webkit-scrollbar-fixing.yml b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml
new file mode 100644
index 00000000000..526a9f25486
--- /dev/null
+++ b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix horizontal scrollbar overlapping on horizontal scrolling-tabs
+merge_request: 23167
+author: Harry Kiselev
+type: other
diff --git a/changelogs/unreleased/order-of-notification-settings.yml b/changelogs/unreleased/order-of-notification-settings.yml
new file mode 100644
index 00000000000..0f0243bcb40
--- /dev/null
+++ b/changelogs/unreleased/order-of-notification-settings.yml
@@ -0,0 +1,5 @@
+---
+title: reorder notification settings by noisy-ness
+merge_request:
+author: C.J. Jameson
+type: changed
diff --git a/changelogs/unreleased/osw-fix-grouping-by-file-path.yml b/changelogs/unreleased/osw-fix-grouping-by-file-path.yml
new file mode 100644
index 00000000000..dff3116e7c6
--- /dev/null
+++ b/changelogs/unreleased/osw-fix-grouping-by-file-path.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid 500's when serializing legacy diff notes
+merge_request: 23544
+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/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/remove-deployment-status-hack-from-backend.yml b/changelogs/unreleased/remove-deployment-status-hack-from-backend.yml
new file mode 100644
index 00000000000..2348bfab7d9
--- /dev/null
+++ b/changelogs/unreleased/remove-deployment-status-hack-from-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Return real deployment status to frontend
+merge_request: 23270
+author:
+type: fixed
diff --git a/changelogs/unreleased/render-text-deprecated.yml b/changelogs/unreleased/render-text-deprecated.yml
new file mode 100644
index 00000000000..7dbbd13bcef
--- /dev/null
+++ b/changelogs/unreleased/render-text-deprecated.yml
@@ -0,0 +1,6 @@
+---
+title: 'Fix deprecation: render :text is deprecated because it does not actually render
+ a text/plain response'
+merge_request: 23425
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/retryable_create_or_update_kubernetes_namespace.yml b/changelogs/unreleased/retryable_create_or_update_kubernetes_namespace.yml
new file mode 100644
index 00000000000..607f2709f90
--- /dev/null
+++ b/changelogs/unreleased/retryable_create_or_update_kubernetes_namespace.yml
@@ -0,0 +1,6 @@
+---
+title: Updates service to update Kubernetes project namespaces and restricted service
+ account if present
+merge_request: 23525
+author:
+type: changed
diff --git a/changelogs/unreleased/security-182-update-workhorse.yml b/changelogs/unreleased/security-182-update-workhorse.yml
new file mode 100644
index 00000000000..76850901b68
--- /dev/null
+++ b/changelogs/unreleased/security-182-update-workhorse.yml
@@ -0,0 +1,5 @@
+---
+title: Redact sensitive information on gitlab-workhorse log
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-2736-prometheus-ssrf.yml b/changelogs/unreleased/security-2736-prometheus-ssrf.yml
new file mode 100644
index 00000000000..9d0dda8a75f
--- /dev/null
+++ b/changelogs/unreleased/security-2736-prometheus-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Do not follow redirects in Prometheus service when making http requests to the configured api url
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml b/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml
new file mode 100644
index 00000000000..0361fb0c041
--- /dev/null
+++ b/changelogs/unreleased/security-bvl-exposure-in-commits-list.yml
@@ -0,0 +1,5 @@
+---
+title: Don't expose confidential information in commit message list
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-email-change-notification.yml b/changelogs/unreleased/security-email-change-notification.yml
new file mode 100644
index 00000000000..45075ff20bb
--- /dev/null
+++ b/changelogs/unreleased/security-email-change-notification.yml
@@ -0,0 +1,5 @@
+---
+title: Provide email notification when a user changes their email address
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-pat-web-access.yml b/changelogs/unreleased/security-fix-pat-web-access.yml
new file mode 100644
index 00000000000..62ffb908fe5
--- /dev/null
+++ b/changelogs/unreleased/security-fix-pat-web-access.yml
@@ -0,0 +1,5 @@
+---
+title: Restrict Personal Access Tokens to API scope on web requests
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-uri-xss-applications.yml b/changelogs/unreleased/security-fix-uri-xss-applications.yml
new file mode 100644
index 00000000000..0eaa1b1c4a3
--- /dev/null
+++ b/changelogs/unreleased/security-fix-uri-xss-applications.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve reflected XSS in Ouath authorize window
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml b/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml
new file mode 100644
index 00000000000..32c85a2a7da
--- /dev/null
+++ b/changelogs/unreleased/security-fix-webhook-ssrf-ipv6.yml
@@ -0,0 +1,5 @@
+---
+title: Fix SSRF in project integrations
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fj-crlf-injection.yml b/changelogs/unreleased/security-fj-crlf-injection.yml
new file mode 100644
index 00000000000..861167b8a6e
--- /dev/null
+++ b/changelogs/unreleased/security-fj-crlf-injection.yml
@@ -0,0 +1,5 @@
+---
+title: Fix CRLF vulnerability in Project hooks
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-guest-comments.yml b/changelogs/unreleased/security-guest-comments.yml
new file mode 100644
index 00000000000..2c99512433b
--- /dev/null
+++ b/changelogs/unreleased/security-guest-comments.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed ability to comment on locked/confidential issues.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-guest-comments_2.yml b/changelogs/unreleased/security-guest-comments_2.yml
new file mode 100644
index 00000000000..be6f2d6a490
--- /dev/null
+++ b/changelogs/unreleased/security-guest-comments_2.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed ability of guest users to edit/delete comments on locked or confidential issues.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-issue_51301.yml b/changelogs/unreleased/security-issue_51301.yml
new file mode 100644
index 00000000000..cf8ebb54b1c
--- /dev/null
+++ b/changelogs/unreleased/security-issue_51301.yml
@@ -0,0 +1,5 @@
+---
+title: Fix milestone promotion authorization check
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-mermaid-xss.yml b/changelogs/unreleased/security-mermaid-xss.yml
new file mode 100644
index 00000000000..bcf93ef37ff
--- /dev/null
+++ b/changelogs/unreleased/security-mermaid-xss.yml
@@ -0,0 +1,5 @@
+---
+title: Configure mermaid to not render HTML content in diagrams
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-pages-toctou-race.yml b/changelogs/unreleased/security-pages-toctou-race.yml
new file mode 100644
index 00000000000..1c055f6087f
--- /dev/null
+++ b/changelogs/unreleased/security-pages-toctou-race.yml
@@ -0,0 +1,6 @@
+---
+title: Fix a possible symlink time of check to time of use race condition in GitLab
+ Pages
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-private-group.yml b/changelogs/unreleased/security-private-group.yml
new file mode 100644
index 00000000000..dbb7794dfed
--- /dev/null
+++ b/changelogs/unreleased/security-private-group.yml
@@ -0,0 +1,6 @@
+---
+title: Removed ability to see private group names when the group id is entered in
+ the url.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-stored-xss-for-environments.yml b/changelogs/unreleased/security-stored-xss-for-environments.yml
new file mode 100644
index 00000000000..5d78ca00942
--- /dev/null
+++ b/changelogs/unreleased/security-stored-xss-for-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Fix stored XSS for Environments
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml b/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml
new file mode 100644
index 00000000000..3bd8123a346
--- /dev/null
+++ b/changelogs/unreleased/security-xss-in-markdown-following-unrecognized-html-element.yml
@@ -0,0 +1,5 @@
+---
+title: Fix possible XSS attack in Markdown urls with spaces
+merge_request: 2599
+author:
+type: security
diff --git a/changelogs/unreleased/set-kubeconfig-nil-when-token-nil.yml b/changelogs/unreleased/set-kubeconfig-nil-when-token-nil.yml
new file mode 100644
index 00000000000..6eac2a0146c
--- /dev/null
+++ b/changelogs/unreleased/set-kubeconfig-nil-when-token-nil.yml
@@ -0,0 +1,5 @@
+---
+title: Make KUBECONFIG nil if KUBE_TOKEN is nil
+merge_request: 23414
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-disable-autocomplete-mirror-settings.yml b/changelogs/unreleased/sh-disable-autocomplete-mirror-settings.yml
new file mode 100644
index 00000000000..e42906e88f2
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-autocomplete-mirror-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Disable password autocomplete in mirror form fill
+merge_request: 23402
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-hash-filename-handling.yml b/changelogs/unreleased/sh-fix-hash-filename-handling.yml
deleted file mode 100644
index cb32051a4ab..00000000000
--- a/changelogs/unreleased/sh-fix-hash-filename-handling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix handling of filenames with hash characters in tree view
-merge_request: 23368
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-38317.yml b/changelogs/unreleased/sh-fix-issue-38317.yml
new file mode 100644
index 00000000000..13fcb5b8f96
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-38317.yml
@@ -0,0 +1,5 @@
+---
+title: Remove needless auto-capitalization on Wiki page titles
+merge_request: 23288
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml b/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml
new file mode 100644
index 00000000000..627de25650d
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "protected branches only" checkbox not set properly at init
+merge_request: 23409
+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-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-full-path-config.yml b/changelogs/unreleased/tc-backfill-full-path-config.yml
new file mode 100644
index 00000000000..4f06284d0e3
--- /dev/null
+++ b/changelogs/unreleased/tc-backfill-full-path-config.yml
@@ -0,0 +1,5 @@
+---
+title: Migration to write fullpath in all repository configs
+merge_request: 22322
+author:
+type: other
diff --git a/changelogs/unreleased/tc-repo-full-path-in-db.yml b/changelogs/unreleased/tc-repo-full-path-in-db.yml
new file mode 100644
index 00000000000..ead8feabeb9
--- /dev/null
+++ b/changelogs/unreleased/tc-repo-full-path-in-db.yml
@@ -0,0 +1,5 @@
+---
+title: Add model and relation to store repo full path in database
+merge_request: 23143
+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/unicorn-monkey-patch.yml b/changelogs/unreleased/unicorn-monkey-patch.yml
new file mode 100644
index 00000000000..6b0e00ca291
--- /dev/null
+++ b/changelogs/unreleased/unicorn-monkey-patch.yml
@@ -0,0 +1,5 @@
+---
+title: Add monkey patch to unicorn to fix eof? problem
+merge_request: 23385
+author:
+type: fixed
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/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-collapse-discussions.yml b/changelogs/unreleased/winh-collapse-discussions.yml
new file mode 100644
index 00000000000..19d04506318
--- /dev/null
+++ b/changelogs/unreleased/winh-collapse-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Fix collapsing discussion replies
+merge_request: 23462
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-divider-margin.yml b/changelogs/unreleased/winh-divider-margin.yml
new file mode 100644
index 00000000000..db84090c15c
--- /dev/null
+++ b/changelogs/unreleased/winh-divider-margin.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust divider margin to comply with design specs
+merge_request: 23548
+author:
+type: changed
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-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-merge-request-commit-discussion.yml b/changelogs/unreleased/winh-merge-request-commit-discussion.yml
new file mode 100644
index 00000000000..b0c6264369b
--- /dev/null
+++ b/changelogs/unreleased/winh-merge-request-commit-discussion.yml
@@ -0,0 +1,5 @@
+---
+title: Display commit ID for commit diff discussion on merge request
+merge_request: 23370
+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/workhorse-7-3-0.yml b/changelogs/unreleased/workhorse-7-3-0.yml
new file mode 100644
index 00000000000..6708b8a3cbb
--- /dev/null
+++ b/changelogs/unreleased/workhorse-7-3-0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade GitLab Workhorse to v7.3.0
+merge_request: 23489
+author:
+type: other
diff --git a/config.ru b/config.ru
index 405d01863ac..a5d055334dd 100644
--- a/config.ru
+++ b/config.ru
@@ -13,6 +13,10 @@ if defined?(Unicorn)
# Max memory size (RSS) per worker
use Unicorn::WorkerKiller::Oom, min, max
end
+
+ # Monkey patch for fixing Rack 2.0.6 bug:
+ # https://gitlab.com/gitlab-org/gitlab-ee/issues/8539
+ Unicorn::StreamInput.send(:public, :eof?) # rubocop:disable GitlabSecurity/PublicSend
end
require ::File.expand_path('../config/environment', __FILE__)
diff --git a/config/application.rb b/config/application.rb
index 5804d8fd27b..63a5b483fc2 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -103,6 +103,9 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - File content from Web Editor (:content)
+ #
+ # NOTE: It is **IMPORTANT** to also update gitlab-workhorse's filter when adding parameters here to not
+ # introduce another security vulnerability: https://gitlab.com/gitlab-org/gitlab-workhorse/issues/182
config.filter_parameters += [/token$/, /password/, /secret/, /key$/]
config.filter_parameters += %i(
certificate
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 84d47bd52ad..6e4f7ce30a0 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,9 @@
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:54.840151000 Z
+- - :approve
+ - echarts
+ - :who: Mike Greiling
+ :why: https://github.com/apache/incubator-echarts/blob/master/LICENSE
+ :versions: []
+ :when: 2018-12-05 22:12:30.550027000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 09e21b2c6f2..1c16b999e55 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -114,6 +114,9 @@ production: &base
# The default is 'shared/cache/archive/' relative to the root of the Rails app.
# repository_downloads_path: shared/cache/archive/
+ ## Impersonation settings
+ impersonation_enabled: true
+
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
# For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
@@ -545,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 bd02b85c7ce..db35fa96ea2 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -153,6 +153,7 @@ Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
+Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
#
@@ -301,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/action_dispatch_http_mime_negotiation.rb b/config/initializers/action_dispatch_http_mime_negotiation.rb
new file mode 100644
index 00000000000..bdf5b0babfb
--- /dev/null
+++ b/config/initializers/action_dispatch_http_mime_negotiation.rb
@@ -0,0 +1,19 @@
+# Starting with Rails 5, Rails tries to determine the request format based on
+# the extension of the full URL path if no explicit `format` param or `Accept`
+# header is provided, like when simply browsing to a page in your browser.
+#
+# This is undesireable in GitLab, because many of our paths will end in a ref or
+# blob name that can end with any extension, while these pages should still be
+# presented as HTML unless otherwise specified.
+
+# We override `format_from_path_extension` to disable this behavior.
+
+module ActionDispatch
+ module Http
+ module MimeNegotiation
+ def format_from_path_extension
+ nil
+ end
+ end
+ end
+end
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/devise.rb b/config/initializers/devise.rb
index 179e00cdbd0..67eabb0b4fc 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -103,6 +103,9 @@ Devise.setup do |config|
# Send a notification email when the user's password is changed
config.send_password_change_notification = true
+ # Send a notification email when the user's email is changed
+ config.send_email_changed_notification = true
+
# ==> Configuration for :validatable
# Range for password length. Default is 6..128.
config.password_length = 8..128
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index f321b4ea763..6be5c00daaa 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -48,6 +48,13 @@ Doorkeeper.configure do
#
force_ssl_in_redirect_uri false
+ # Specify what redirect URI's you want to block during Application creation.
+ # Any redirect URI is whitelisted by default.
+ #
+ # You can use this option in order to forbid URI's with 'javascript' scheme
+ # for example.
+ forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) }
+
# Provide support for an owner to be assigned to each registered application (disabled by default)
# Optional parameter confirmation: true (default false) if you want to enforce ownership of
# a registered application
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/rack_attack_global.rb b/config/initializers/rack_attack_global.rb
index 45963831c41..86cb930eca9 100644
--- a/config/initializers/rack_attack_global.rb
+++ b/config/initializers/rack_attack_global.rb
@@ -33,22 +33,22 @@ class Rack::Attack
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
req.api_request? &&
- req.authenticated_user_id
+ req.authenticated_user_id([:api])
end
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
req.web_request? &&
- req.authenticated_user_id
+ req.authenticated_user_id([:api, :rss, :ics])
end
class Request
def unauthenticated?
- !authenticated_user_id
+ !authenticated_user_id([:api, :rss, :ics])
end
- def authenticated_user_id
- Gitlab::Auth::RequestAuthenticator.new(self).user&.id
+ def authenticated_user_id(request_formats)
+ Gitlab::Auth::RequestAuthenticator.new(self).user(request_formats)&.id
end
def api_request?
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/jest.config.js b/config/jest.config.js
new file mode 100644
index 00000000000..23e62f49be1
--- /dev/null
+++ b/config/jest.config.js
@@ -0,0 +1,27 @@
+/* eslint-disable filenames/match-regex */
+
+const reporters = ['default'];
+
+if (process.env.CI) {
+ reporters.push([
+ 'jest-junit',
+ {
+ output: './junit_jest.xml',
+ },
+ ]);
+}
+
+// eslint-disable-next-line import/no-commonjs
+module.exports = {
+ testMatch: ['<rootDir>/spec/frontend/**/*_spec.js'],
+ moduleNameMapper: {
+ '^~(.*)$': '<rootDir>/app/assets/javascripts$1',
+ },
+ collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
+ coverageDirectory: '<rootDir>/coverage-frontend/',
+ coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
+ cacheDirectory: '<rootDir>/tmp/cache/jest',
+ modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
+ reporters,
+ rootDir: '..', // necessary because this file is in the config/ subdirectory
+};
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/routes/wiki.rb b/config/routes/wiki.rb
index 1a07b1c206b..2ca52e55fca 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -6,7 +6,7 @@ scope(controller: :wikis) do
post '/', to: 'wikis#create'
end
- scope(path: 'wikis/*id', as: :wiki, format: false, defaults: { format: :html }) do
+ scope(path: 'wikis/*id', as: :wiki, format: false) do
get :edit
get :history
post :preview_markdown
diff --git a/config/settings.rb b/config/settings.rb
index 3f3481bb65d..1b94df785a7 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -95,6 +95,14 @@ class Settings < Settingslogic
Gitlab::Application.secrets.db_key_base[0..31]
end
+ def attr_encrypted_db_key_base_32
+ Gitlab::Utils.ensure_utf8_size(attr_encrypted_db_key_base, bytes: 32.bytes)
+ end
+
+ def attr_encrypted_db_key_base_12
+ Gitlab::Utils.ensure_utf8_size(attr_encrypted_db_key_base, bytes: 12.bytes)
+ end
+
# This should be used for :per_attribute_salt_and_iv mode. There is no
# need to truncate the key because the encryptor will use the salt to
# generate a hash of the password:
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 53e1c8778b6..4782a223561 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -81,3 +81,5 @@
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
- [auto_devops, 2]
+ - [repository_cleanup, 1]
+ - [delete_stored_files, 1]
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 713ed95a04c..530c6638653 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -2,7 +2,7 @@
require 'yaml'
-NO_CHANGELOG_LABELS = %w[backstage Documentation QA test].freeze
+NO_CHANGELOG_LABELS = %w[backstage ci-build Documentation meta QA test].freeze
SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze
CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze
You can create one with:
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/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 5af77c49913..bdc0a2db7db 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -104,7 +104,7 @@ class Gitlab::Seeder::Pipelines
def create_pipeline!(project, ref, commit)
- project.pipelines.create!(sha: commit.id, ref: ref, source: :push)
+ project.ci_pipelines.create!(sha: commit.id, ref: ref, source: :push)
end
def build_create!(pipeline, opts = {})
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/20181108091549_cleanup_environments_external_url.rb b/db/migrate/20181108091549_cleanup_environments_external_url.rb
new file mode 100644
index 00000000000..8d6c20a4b15
--- /dev/null
+++ b/db/migrate/20181108091549_cleanup_environments_external_url.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CleanupEnvironmentsExternalUrl < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:environments, :external_url, nil) do |table, query|
+ query.where(table[:external_url].matches('javascript://%'))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
new file mode 100644
index 00000000000..36d9ad45b19
--- /dev/null
+++ b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddEncryptedRunnersTokenToSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :runners_registration_token_encrypted, :string
+ end
+end
diff --git a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
new file mode 100644
index 00000000000..b92b1b50218
--- /dev/null
+++ b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddEncryptedRunnersTokenToNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :runners_token_encrypted, :string
+ end
+end
diff --git a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
new file mode 100644
index 00000000000..53e475bd180
--- /dev/null
+++ b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddEncryptedRunnersTokenToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :runners_token_encrypted, :string
+ end
+end
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
new file mode 100644
index 00000000000..65476109c61
--- /dev/null
+++ b/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddMergeRequestIdToCiPipelines < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ add_column :ci_pipelines, :merge_request_id, :integer
+ end
+
+ def down
+ remove_column :ci_pipelines, :merge_request_id, :integer
+ end
+end
diff --git a/db/migrate/20181120082911_rename_repositories_pool_repositories.rb b/db/migrate/20181120082911_rename_repositories_pool_repositories.rb
new file mode 100644
index 00000000000..165771c4775
--- /dev/null
+++ b/db/migrate/20181120082911_rename_repositories_pool_repositories.rb
@@ -0,0 +1,11 @@
+class RenameRepositoriesPoolRepositories < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # This change doesn't require downtime as the table is not in use, so we're
+ # free to change an empty table
+ DOWNTIME = false
+
+ def change
+ rename_table :repositories, :pool_repositories
+ end
+end
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
new file mode 100644
index 00000000000..03f677a4678
--- /dev/null
+++ b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ 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
+
+ def down
+ if foreign_key_exists?(:ci_pipelines, :merge_requests, column: :merge_request_id)
+ remove_foreign_key :ci_pipelines, :merge_requests
+ end
+
+ 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
new file mode 100644
index 00000000000..40db6b399ab
--- /dev/null
+++ b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddTokenEncryptedToCiRunners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_runners, :token_encrypted, :string
+ end
+end
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..5b47a279438
--- /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
+ 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..a0a02e81323
--- /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
+ 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/20181122160027_create_project_repositories.rb b/db/migrate/20181122160027_create_project_repositories.rb
new file mode 100644
index 00000000000..e42cef9b1c6
--- /dev/null
+++ b/db/migrate/20181122160027_create_project_repositories.rb
@@ -0,0 +1,18 @@
+# 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 CreateProjectRepositories < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_repositories, id: :bigserial do |t|
+ t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
+ t.string :disk_path, null: false, index: { unique: true }
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ end
+ end
+end
diff --git a/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb b/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb
new file mode 100644
index 00000000000..bcd969e91c5
--- /dev/null
+++ b/db/migrate/20181123135036_drop_not_null_constraint_pool_repository_disk_path.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class DropNotNullConstraintPoolRepositoryDiskPath < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ change_column_null :pool_repositories, :disk_path, true
+ end
+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/20181010133639_backfill_store_project_full_path_in_repo.rb b/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb
new file mode 100644
index 00000000000..e9ab45ae9a1
--- /dev/null
+++ b/db/post_migrate/20181010133639_backfill_store_project_full_path_in_repo.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class BackfillStoreProjectFullPathInRepo < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ DELAY_INTERVAL = 5.minutes
+ UP_MIGRATION = 'BackfillProjectFullpathInRepoConfig::Up'
+ DOWN_MIGRATION = 'BackfillProjectFullpathInRepoConfig::Down'
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ include EachBatch
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Project, UP_MIGRATION, DELAY_INTERVAL)
+ end
+
+ def down
+ queue_background_migration_jobs_by_range_at_intervals(Project, DOWN_MIGRATION, DELAY_INTERVAL)
+ end
+end
diff --git a/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb
new file mode 100644
index 00000000000..ff5510e8eb7
--- /dev/null
+++ b/db/post_migrate/20181026091631_migrate_forbidden_redirect_uris.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class MigrateForbiddenRedirectUris < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ FORBIDDEN_SCHEMES = %w[data:// vbscript:// javascript://]
+ NEW_URI = 'http://forbidden-scheme-has-been-overwritten'
+
+ disable_ddl_transaction!
+
+ def up
+ update_forbidden_uris(:oauth_applications)
+ update_forbidden_uris(:oauth_access_grants)
+ end
+
+ def down
+ # noop
+ end
+
+ private
+
+ def update_forbidden_uris(table_name)
+ update_column_in_batches(table_name, :redirect_uri, NEW_URI) do |table, query|
+ where_clause = FORBIDDEN_SCHEMES.map do |scheme|
+ table[:redirect_uri].matches("#{scheme}%")
+ end.inject(&:or)
+
+ query.where(where_clause)
+ end
+ end
+end
diff --git a/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb b/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb
new file mode 100644
index 00000000000..753e052f7a7
--- /dev/null
+++ b/db/post_migrate/20181121111200_schedule_runners_token_encryption.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ScheduleRunnersTokenEncryption < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10000
+ RANGE_SIZE = 2000
+ MIGRATION = 'EncryptRunnersTokens'
+
+ MODELS = [
+ ::Gitlab::BackgroundMigration::Models::EncryptColumns::Settings,
+ ::Gitlab::BackgroundMigration::Models::EncryptColumns::Namespace,
+ ::Gitlab::BackgroundMigration::Models::EncryptColumns::Project,
+ ::Gitlab::BackgroundMigration::Models::EncryptColumns::Runner
+ ].freeze
+
+ disable_ddl_transaction!
+
+ def up
+ MODELS.each do |model|
+ model.each_batch(of: BATCH_SIZE) do |relation, index|
+ delay = index * 4.minutes
+
+ relation.each_batch(of: RANGE_SIZE) do |relation|
+ range = relation.pluck('MIN(id)', 'MAX(id)').first
+ args = [model.name.demodulize.downcase, *range]
+
+ BackgroundMigrationWorker.perform_in(delay, MIGRATION, args)
+ end
+ end
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20181123042307_drop_site_statistics.rb b/db/post_migrate/20181123042307_drop_site_statistics.rb
index 8986374ef65..8986374ef65 100644
--- a/db/migrate/20181123042307_drop_site_statistics.rb
+++ b/db/post_migrate/20181123042307_drop_site_statistics.rb
diff --git a/db/schema.rb b/db/schema.rb
index 9c9c19aa897..d7124100621 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: 20181203002526) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -166,6 +166,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
t.string "commit_email_hostname"
+ t.string "runners_registration_token_encrypted"
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -344,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
@@ -352,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
@@ -359,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
@@ -473,7 +477,9 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "protected"
t.integer "failure_reason"
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", 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
@@ -520,6 +526,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.string "ip_address"
t.integer "maximum_timeout"
t.integer "runner_type", limit: 2, null: false
+ t.string "token_encrypted"
t.index ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
t.index ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree
t.index ["locked"], name: "index_ci_runners_on_locked", using: :btree
@@ -1335,6 +1342,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.integer "two_factor_grace_period", default: 48, null: false
t.integer "cached_markdown_version"
t.string "runners_token"
+ t.string "runners_token_encrypted"
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
t.index ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -1501,6 +1509,13 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
end
+ create_table "pool_repositories", id: :bigserial, force: :cascade do |t|
+ t.integer "shard_id", null: false
+ t.string "disk_path"
+ 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
+ end
+
create_table "programming_languages", force: :cascade do |t|
t.string "name", null: false
t.string "color", null: false
@@ -1594,6 +1609,15 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["status"], name: "index_project_mirror_data_on_status", using: :btree
end
+ create_table "project_repositories", id: :bigserial, force: :cascade do |t|
+ t.integer "shard_id", null: false
+ t.string "disk_path", null: false
+ t.integer "project_id", null: false
+ t.index ["disk_path"], name: "index_project_repositories_on_disk_path", unique: true, using: :btree
+ t.index ["project_id"], name: "index_project_repositories_on_project_id", unique: true, using: :btree
+ t.index ["shard_id"], name: "index_project_repositories_on_shard_id", using: :btree
+ end
+
create_table "project_statistics", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
@@ -1659,6 +1683,8 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.boolean "pages_https_only", default: true
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
@@ -1798,13 +1824,6 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree
end
- create_table "repositories", id: :bigserial, force: :cascade do |t|
- t.integer "shard_id", null: false
- t.string "disk_path", null: false
- t.index ["disk_path"], name: "index_repositories_on_disk_path", unique: true, using: :btree
- t.index ["shard_id"], name: "index_repositories_on_shard_id", using: :btree
- end
-
create_table "repository_languages", id: false, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "programming_language_id", null: false
@@ -2279,6 +2298,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
+ add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade
@@ -2373,6 +2393,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", "shards", on_delete: :restrict
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
@@ -2384,8 +2405,10 @@ ActiveRecord::Schema.define(version: 20181126153547) do
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
+ add_foreign_key "project_repositories", "projects", on_delete: :cascade
+ add_foreign_key "project_repositories", "shards", on_delete: :restrict
add_foreign_key "project_statistics", "projects", on_delete: :cascade
- add_foreign_key "projects", "repositories", column: "pool_repository_id", name: "fk_6e5c14658a", on_delete: :nullify
+ add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches", name: "fk_8a3072ccb3", on_delete: :cascade
add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade
@@ -2397,7 +2420,6 @@ ActiveRecord::Schema.define(version: 20181126153547) do
add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "remote_mirrors", "projects", on_delete: :cascade
- add_foreign_key "repositories", "shards", on_delete: :restrict
add_foreign_key "repository_languages", "projects", on_delete: :cascade
add_foreign_key "resource_label_events", "issues", on_delete: :cascade
add_foreign_key "resource_label_events", "labels", on_delete: :nullify
diff --git a/doc/README.md b/doc/README.md
index bf93c73843f..b15c3a63d92 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,12 +3,18 @@ comments: false
description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
+<div class="display-none">
+ <em>Visit <a href="https://docs.gitlab.com/ce/">docs.gitlab.com</a> for optimized
+ navigation, discoverability, and readability.</em>
+</div>
+<!-- the div above will not display on the docs site but will display on /help -->
+
# GitLab Documentation
Welcome to [GitLab](https://about.gitlab.com/) Documentation.
Here you can access the complete documentation for GitLab, the single application for the
-[entire DevOps lifecycle](#complete-devops-with-gitlab).
+[entire DevOps lifecycle](#the-entire-devops-lifecycle).
## Overview
@@ -72,11 +78,11 @@ GitLab provides statistics and insight into ways you can maximize the value of G
The following documentation relates to the DevOps **Manage** stage:
-| Manage Topics | Description |
-|:----------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [Authentication and Authorization](administration/auth/README.md) **[CORE ONLY]** | Supported authentication and authorization providers. |
-| [GitLab Cycle Analytics](user/project/cycle_analytics.md) | Measure the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have. |
-| [Instance Statistics](user/instance_statistics/index.md) | Discover statistics on how many GitLab features you use and user activity. |
+| Manage Topics | Description |
+|:--------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Authentication and<br/>Authorization](administration/auth/README.md) **[CORE ONLY]** | Supported authentication and authorization providers. |
+| [GitLab Cycle Analytics](user/project/cycle_analytics.md) | Measure the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have. |
+| [Instance Statistics](user/instance_statistics/index.md) | Discover statistics on how many GitLab features you use and user activity. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -93,17 +99,17 @@ management tools.
The following documentation relates to the DevOps **Plan** stage:
-| Plan Topics | Description |
-|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
-| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. |
-| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
-| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
-| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md), [issue and merge request templates](user/project/description_templates.md), and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
-| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. |
-| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. |
-| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
-| [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. |
-| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. |
+| Plan Topics | Description |
+|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. |
+| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
+| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
+| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
+| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. |
+| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. |
+| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
+| [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. |
+| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -124,16 +130,16 @@ The following documentation relates to the DevOps **Create** stage:
#### Projects and Groups
-| Create Topics - Projects and Groups | Description |
-|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------|
-| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and [import and export projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. |
-| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. |
-| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. |
-| [Projects](user/project/index.md), including [project access](public_access/public_access.md) and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. |
-| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. |
-| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. |
-| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. |
-| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. |
+| Create Topics - Projects and Groups | Description |
+|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------|
+| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export<br/>projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. |
+| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. |
+| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. |
+| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. |
+| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. |
+| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. |
+| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. |
+| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -143,18 +149,18 @@ The following documentation relates to the DevOps **Create** stage:
#### Repositories
-| Create Topics - Repositories | Description |
-|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------|
-| [Branches](user/project/repository/branches/index.md) and the [default branch](user/project/repository/branches/index.md#default-branch) | How to use branches in GitLab. |
-| [Commits](user/project/repository/index.md#commits) and [signing commits](user/project/repository/gpg_signed_commits/index.md) | Work with commits, and use GPG to sign your commits. |
-| [Create branches](user/project/repository/web_editor.md#create-a-new-branch), [create](user/project/repository/web_editor.md#create-a-file) and [upload](user/project/repository/web_editor.md#upload-a-file) files, and [create directories](user/project/repository/web_editor.md#create-a-directory) | Create branches, create and upload files, and create directories within GitLab. |
-| [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) | Bulk delete branches after their changes are merged. |
-| [File templates](user/project/repository/web_editor.md#template-dropdowns) | File templates for common files. |
-| [Files](user/project/repository/index.md#files) | Files management. |
-| [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. |
-| [Protected branches](user/project/protected_branches.md) | Use protected branches. |
-| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. |
-| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. |
+| Create Topics - Repositories | Description |
+|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------|
+| [Branches](user/project/repository/branches/index.md) and the [default branch](user/project/repository/branches/index.md#default-branch) | How to use branches in GitLab. |
+| [Commits](user/project/repository/index.md#commits) and [signing commits](user/project/repository/gpg_signed_commits/index.md) | Work with commits, and use GPG to sign your commits. |
+| [Create branches](user/project/repository/web_editor.md#create-a-new-branch), [create](user/project/repository/web_editor.md#create-a-file)<br/>and [upload](user/project/repository/web_editor.md#upload-a-file) files, and [create directories](user/project/repository/web_editor.md#create-a-directory) | Create branches, create and upload files, and create directories within GitLab. |
+| [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) | Bulk delete branches after their changes are merged. |
+| [File templates](user/project/repository/web_editor.md#template-dropdowns) | File templates for common files. |
+| [Files](user/project/repository/index.md#files) | Files management. |
+| [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. |
+| [Protected branches](user/project/protected_branches.md) | Use protected branches. |
+| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. |
+| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -268,15 +274,15 @@ configuration. Then customize everything from buildpacks to CI/CD.
The following documentation relates to the DevOps **Configure** stage:
-| Configure Topics | Description |
-|:-------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
-| [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. |
-| [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) | Use Google Kubernetes Engine and GitLab. |
-| [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. |
-| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
-| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
-| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. |
-| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
+| Configure Topics | Description |
+|:-----------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
+| [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. |
+| [Easy creation of Kubernetes<br/>clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) | Use Google Kubernetes Engine and GitLab. |
+| [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. |
+| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
+| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
+| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. |
+| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
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 3395c503ced..dc6a71e2ebd 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -237,3 +237,14 @@ gitaly_enabled=false
When you run `service gitlab restart` Gitaly will be disabled on this
particular machine.
+
+## Troubleshooting Gitaly in production
+
+Since GitLab 11.6, Gitaly comes with a command-line tool called
+`gitaly-debug` that can be run on a Gitaly server to aid in
+troubleshooting. In GitLab 11.6 its only sub-command is
+`simulate-http-clone` which allows you to measure the maximum possible
+Git clone speed for a specific repository on the server.
+
+For an up to date list of sub-commands see [the gitaly-debug
+README](https://gitlab.com/gitlab-org/gitaly/blob/master/cmd/gitaly-debug/README.md).
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
index c358dfbead2..dfd9be3d04c 100644
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -1,16 +1,17 @@
# Request Profiling
## Procedure
+
1. Grab the profiling token from `Monitoring > Requests Profiles` admin page
-(highlighted in a blue in the image below).
-![Profile token](img/request_profiling_token.png)
-1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use any of these tools
- * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension
- * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension
- * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`
+ (highlighted in a blue in the image below).
+ ![Profile token](img/request_profiling_token.png)
+1. Pass the header `X-Profile-Token: <token>` to the request you want to profile. You can use:
+ - Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension.
+ - `curl`. For example, `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`.
1. Once request is finished (which will take a little longer than usual), you can
-view the profiling output from `Monitoring > Requests Profiles` admin page.
-![Profiling output](img/request_profile_result.png)
+ view the profiling output from `Monitoring > Requests Profiles` admin page.
+ ![Profiling output](img/request_profile_result.png)
## Cleaning up
+
Profiling output will be cleared out every day via a Sidekiq worker.
diff --git a/doc/api/README.md b/doc/api/README.md
index 0e8d1aaf86e..b49c3a198f1 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -225,6 +225,42 @@ For more information, refer to the
Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `Private-Token` header.
+#### Disable impersonation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/40385) in GitLab
+11.6.
+
+By default, impersonation is enabled. To disable impersonation:
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['impersonation_enabled'] = false
+ ```
+
+1. Save the file and [reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
+ GitLab for the changes to take effect.
+
+To re-enable impersonation, remove this configuration and reconfigure GitLab.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ impersonation_enabled: false
+ ```
+
+1. Save the file and [restart](../administration/restart_gitlab.md#installations-from-source)
+ GitLab for the changes to take effect.
+
+To re-enable impersonation, remove this configuration and restart GitLab.
+
### Sudo
NOTE: **Note:**
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 7d9b52ec24f..6c16216429d 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -87,7 +87,7 @@ POST /projects/:id/repository/commits
| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update`, `chmod`|
| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb`. Only considered for `move` action. |
-| `content` | string | no | File content, required for all except `delete` and `chmod`. Optional for `move` |
+| `content` | string | no | File content, required for all except `delete`, `chmod`, and `move`. Move actions that do not specify `content` will preserve the existing file content, and any other value of `content` will overwrite the file content. |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
| `execute_filemode` | boolean | no | When `true/false` enables/disables the execute flag on the file. Only considered for `chmod` action. |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index da70c74c4ce..9ff6c73b1b6 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -408,6 +408,7 @@ Parameters:
- `merge_request_iid` (required) - The internal ID of the merge request
- `render_html` (optional) - If `true` response includes rendered HTML for title and description
- `include_diverged_commits_count` (optional) - If `true` response includes the commits behind the target branch
+- `include_rebase_in_progress` (optional) - If `true` response includes whether a rebase operation is in progress
```json
{
@@ -461,6 +462,7 @@ Parameters:
},
"merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
+ "merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -505,7 +507,8 @@ Parameters:
"head_sha": "2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f",
"start_sha": "c380d3acebd181f13629a25d2e2acca46ffe1e00"
},
- "diverged_commits_count": 2
+ "diverged_commits_count": 2,
+ "rebase_in_progress": false
}
```
@@ -773,6 +776,7 @@ POST /projects/:id/merge_requests
},
"merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
+ "merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -900,6 +904,7 @@ Must include at least one non-required attribute from above.
},
"merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
+ "merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -969,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'
@@ -1043,6 +1047,7 @@ Parameters:
},
"merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
+ "merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -1158,6 +1163,7 @@ Parameters:
},
"merge_when_pipeline_succeeds": false,
"merge_status": "can_be_merged",
+ "merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"user_notes_count": 1,
@@ -1206,6 +1212,62 @@ Parameters:
}
```
+## Rebase a merge request
+
+Automatically rebase the `source_branch` of the merge request against its
+`target_branch`.
+
+If you don't have permissions to push to the merge request's source branch -
+you'll get a `403 Forbidden` response.
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/rebase
+```
+
+| 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 |
+| `merge_request_iid` | integer | yes | The internal ID of the merge request |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase
+```
+
+This is an asynchronous request. The API will return an empty `202 Accepted`
+response if the request is enqueued successfully.
+
+You can poll the [Get single MR](#get-single-mr) endpoint with the
+`include_rebase_in_progress` parameter to check the status of the
+asynchronous request.
+
+If the rebase operation is ongoing, the response will include the following:
+
+```json
+{
+ "rebase_in_progress": true
+ "merge_error": null
+}
+```
+
+Once the rebase operation has completed successfully, the response will include
+the following:
+
+```json
+{
+ "rebase_in_progress": false,
+ "merge_error": null,
+}
+```
+
+If the rebase operation fails, the response will include the following:
+
+```json
+{
+ "rebase_in_progress": false,
+ "merge_error": "Rebase failed. Please rebase locally",
+}
+```
+
## Comments on merge requests
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index c8de7f2191d..55f5a4cc3b2 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -108,14 +108,18 @@ Get an archive of the repository. This endpoint can be accessed without
authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/archive
+GET /projects/:id/repository/archive[.format]
```
+`format` is an optional suffix for the archive format. Default is
+`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, 'tbz2`, `tb2`,
+`bz2`, `tar`, and `zip`. For example, specifying `archive.zip`
+would send an archive in ZIP format.
+
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `sha` (optional) - The commit SHA to download. A tag, branch reference or sha can be used. This defaults to the tip of the default branch if not specified
-- `format` (optional) - The archive format. Default is `tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`, `bz2`, `tar`, `zip`
## Compare branches, tags or commits
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c3624f1a535..5f587f480b6 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -4,6 +4,16 @@
**Create, read, update and delete repository files using this API**
+The different scopes available using [personal access tokens](../user/profile/personal_access_tokens.md) are depicted
+in the following table.
+
+| Scope | Description |
+| ----- | ----------- |
+| `read_repository` | Allows read-access to the repository files. |
+| `api` | Allows read-write access to the repository files. |
+
+> `read_repository` scope was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23534) in GitLab 11.6.
+
## Get file from repository
Allows you to receive information about file in repository like name, size,
diff --git a/doc/api/repository_submodules.md b/doc/api/repository_submodules.md
index 2e6797f18f4..11b04c81172 100644
--- a/doc/api/repository_submodules.md
+++ b/doc/api/repository_submodules.md
@@ -22,7 +22,7 @@ PUT /projects/:id/repository/submodules/:submodule
| `commit_message` | string | no | Commit message. If no message is provided, a default one will be set |
```sh
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repositories/submodules/lib%2Fmodules%2Fexample"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/submodules/lib%2Fmodules%2Fexample"
--data "branch=master&commit_sha=3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88&commit_message=Update submodule reference"
```
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/api/snippets.md b/doc/api/snippets.md
index 7892866cd8e..e840e640377 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -37,13 +37,13 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
-``` bash
+```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1
```
Example response:
-``` json
+```json
{
"id": 1,
"title": "test",
@@ -65,6 +65,30 @@ Example response:
}
```
+## Single snippet contents
+
+Get a single snippet's raw contents.
+
+```
+GET /snippets/:id/raw
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1/raw
+```
+
+Example response:
+
+```
+Hello World snippet
+```
+
## Create new snippet
Creates a new snippet. The user must have permission to create new snippets.
@@ -84,7 +108,7 @@ Parameters:
| `visibility` | String | no | The snippet's visibility |
-``` bash
+```bash
curl --request POST \
--data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
--header 'Content-Type: application/json' \
@@ -94,7 +118,7 @@ curl --request POST \
Example response:
-``` json
+```json
{
"id": 1,
"title": "This is a snippet",
@@ -136,7 +160,7 @@ Parameters:
| `visibility` | String | no | The snippet's visibility |
-``` bash
+```bash
curl --request PUT \
--data '{"title": "foo", "content": "bar"}' \
--header 'Content-Type: application/json' \
@@ -146,7 +170,7 @@ curl --request PUT \
Example response:
-``` json
+```json
{
"id": 1,
"title": "test",
@@ -201,13 +225,13 @@ GET /snippets/public
| `per_page` | Integer | no | number of snippets to return per page |
| `page` | Integer | no | the page to retrieve |
-``` bash
+```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1
```
Example response:
-``` json
+```json
[
{
"author": {
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/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 31649ee2792..959271d8abc 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -67,6 +67,9 @@ services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
Any image found at [Docker Hub][hub] or your private Container Registry can be
used as a service.
+Services inherit the same DNS servers, search domains, and additional hosts as
+the CI container itself.
+
You can see some widely used services examples in the relevant documentation of
[CI services examples](../services/README.md).
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index b090ea014dc..b1ccce744d8 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -273,6 +273,8 @@ The `releases` directory will hold all our deployments:
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+ cd {{ $releases_dir }}
+ git reset --hard {{ $commit }}
@endtask
...
@@ -349,6 +351,8 @@ At the end, our `Envoy.blade.php` file will look like this:
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+ cd {{ $releases_dir }}
+ git reset --hard {{ $commit }}
@endtask
@task('run_composer')
@@ -519,7 +523,7 @@ deploy_production:
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- - ~/.composer/vendor/bin/envoy run deploy
+ - ~/.composer/vendor/bin/envoy run deploy --commit="$CI_COMMIT_SHA"
environment:
name: production
url: http://192.168.1.1
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 1ddc1bf4d7e..2c799e83a5f 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
+# Getting started with interactive web terminals **[CORE ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3.
@@ -6,10 +6,9 @@ Interactive web terminals give the user access to a terminal in GitLab for
running one-off commands for their CI pipeline.
NOTE: **Note:**
-This is not available for the shared Runners on GitLab.com.
-To make use of this feature, you need to provide your
-[own Runner](https://docs.gitlab.com/runner/install/) and properly
-[configure it](#configuration).
+GitLab.com does not support interactive web terminal at the moment. Please
+follow [this issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for
+progress.
## Configuration
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/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 77406493578..1277d1fdf8b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -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
@@ -1590,7 +1609,7 @@ Possible values for `when` are:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5.
`parallel` allows you to configure how many instances of a job to run in
-parallel. This value has to be greater than or equal to two (2) and less or equal than 50.
+parallel. This value has to be greater than or equal to two (2) and less than or equal to 50.
This creates N instances of the same job that run in parallel. They're named
sequentially from `job_name 1/N` to `job_name N/N`.
@@ -1821,13 +1840,6 @@ These variables can be later used in all executed commands and scripts.
The YAML-defined variables are also set to all created service containers,
thus allowing to fine tune them.
-To turn off global defined variables in a specific job, define an empty hash:
-
-```yaml
-job_name:
- variables: {}
-```
-
Except for the user defined variables, there are also the ones [set up by the
Runner itself](../variables/README.md#predefined-variables-environment-variables).
One example would be `CI_COMMIT_REF_NAME` which has the value of
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 931a7a8e6d5..e65c5f05505 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -55,7 +55,7 @@ GitLab can be considered to have two layers from a process perspective:
### gitaly
-- [Omnibus confiugration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
+- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
- Layer: Core Service (Data)
Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab. (Think GitLab.com or High Availablity Deployments) As of 11.3.0, This service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 58e08d432cc..e4eb26b3aca 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -1,57 +1,33 @@
# Automatic CE->EE merge
-GitLab Community Edition is merged automatically every 3 hours into the
-Enterprise Edition (look for the [`CE Upstream` merge requests]).
-
-This merge is done automatically in a
-[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679).
-
-## What to do if you are pinged in a `CE Upstream` merge request to resolve a conflict?
-
-1. Please resolve the conflict as soon as possible or ask someone else to do it
- - It's ok to resolve more conflicts than the one that you are asked to resolve.
- In that case, it's a good habit to ask for a double-check on your resolution
- by someone who is familiar with the code you touched.
-1. Once you have resolved your conflicts, push to the branch (no force-push)
-1. Assign the merge request to the next person that has to resolve a conflict
-1. If all conflicts are resolved after your resolution is pushed, keep the merge
- request assigned to you: **you are now responsible for the merge request to be
- green**
-1. If you are the last person to resolve the conflicts, the pipeline is green,
- and you have merge rights, merge the MR, but **do not** choose to squash.
- Otherwise, assign the MR to someone that can merge.
-1. If you need any help, you can ping the current [release managers], or ask in
- the `#ce-to-ee` Slack channel
-
-A few notes about the automatic CE->EE merge job:
-
-- If a merge is already in progress, the job
- [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687).
-- If there is nothing to merge (i.e. EE is up-to-date with CE), the job doesn't
- create a new one
-- The job posts messages to the `#ce-to-ee` Slack channel to inform what's the
- current CE->EE merge status (e.g. "A new MR has been created", "A MR is still pending")
-
-[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream
-[release managers]: https://about.gitlab.com/release-managers/
+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.
## 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 every few
-hours, it can happen that:
+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. 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 automatic merge bot will ping someone to resolve the conflict **that are
- already resolved in the EE merge request that isn't merged yet**
-
-That's a waste of time, and that's why you should merge EE merge request before
-their CE counterpart.
+ merged yet.
+1. The CE changes are reverted.
## Avoiding CE->EE merge conflicts beforehand
@@ -69,136 +45,89 @@ 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 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)
+## 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.
+
+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).
+
+## How it works
+
+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.
+
+## FAQ
+
+### Why?
+
+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.
+
+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.
+
+### My changes are reverted, but I set up an EE MR to resolve conflicts
+
+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).
+
+### My changes keep getting reverted, and this is really annoying!
+
+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.
+
+### Some files I work with often conflict, how can I best deal with this?
+
+If you find you keep running into merge conflicts, consider refactoring the file
+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/code_review.md b/doc/development/code_review.md
index 52710e54e86..7788d155154 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -5,7 +5,7 @@ having your code reviewed.
All merge requests for GitLab CE and EE, whether written by a GitLab team member
or a volunteer contributor, must go through a code review process to ensure the
-code is effective, understandable, and maintainable.
+code is effective, understandable, maintainable, and secure.
## Getting your merge request reviewed, approved, and merged
@@ -20,12 +20,24 @@ importance of involving reviewer(s) in the section on the responsibility of the
If you need some guidance (e.g. it's your first merge request), feel free to ask
one of the [Merge request coaches][team].
+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):
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).
+Getting your merge request **merged** also requires a maintainer. If it requires
+more than one approval, the last maintainer to review and approve it will also merge it.
+
+### Approval guidelines
+
+As described in the section on the responsibility of the maintainer below, you
+are recommended to get your merge request approved and merged by maintainer(s)
+from teams other than your own.
+
1. If your merge request includes backend changes [^1], it must be
**approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**.
1. If your merge request includes frontend changes [^1], it must be
@@ -39,12 +51,14 @@ widget. Reviewers can add their approval by [approving additionally](https://doc
1. If your merge request includes a new dependency or a filesystem change, it must be
**approved by a [Distribution team member][team]**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/) for more details.
-Getting your merge request **merged** also requires a maintainer. If it requires
-more than one approval, the last maintainer to review and approve it will also merge it.
+#### Security requirements
-As described in the section on the responsibility of the maintainer below, you
-are recommended to get your merge request approved and merged by maintainer(s)
-from other teams than your own.
+ 1. If your merge request is processing, storing, or transferring any kind of [RED or ORANGE data](https://docs.google.com/document/d/15eNKGA3zyZazsJMldqTBFbYMnVUSQSpU14lo22JMZQY/edit) (this is a confidential document), it must be
+ **approved by a [Security Engineer][team]**.
+ 1. If your merge request involves implementing, utilizing, or is otherwise related to any type of authentication, authorization, or session handling mechanism, it must be
+ **approved by a [Security Engineer][team]**.
+ 1. If your merge request has a goal which requires a cryptographic function such as: confidentiality, integrity, authentication, or non-repudiation, it must be
+ **approved by a [Security Engineer][team]**.
### The responsibility of the merge request author
@@ -54,9 +68,10 @@ merge request author.
Before assigning a merge request to a maintainer for approval and merge, they
should be confident that it actually solves the problem it was meant to solve,
that it does so in the most appropriate way, that it satisfies all requirements,
-and that there are no remaining bugs, logical problems, or uncovered edge cases.
-The merge request should also have a completed task list in its description and
-a passing CI pipeline to avoid unnecessary back and forth.
+and that there are no remaining bugs, logical problems, uncovered edge cases,
+or known vulnerabilities. The merge request should also have a completed task
+list in its description and a passing CI pipeline to avoid unnecessary back and
+forth.
To reach the required level of confidence in their solution, an author is expected
to involve other people in the investigation and implementation processes as
@@ -72,6 +87,23 @@ If an author is unsure if a merge request needs a domain expert's opinion, that'
usually a pretty good sign that it does, since without it the required level of
confidence in their solution will not have been reached.
+Before the review, the author is requested to submit comments on the merge
+request diff alerting the reviewer to anything important as well as for anything
+that demands further explanation or attention. Examples of content that may
+warrant a comment could be:
+
+- The addition of a linting rule (Rubocop, JS etc)
+- The addition of a library (Ruby gem, JS lib etc)
+- Where not obvious, a link to the parent class or method
+- Any benchmarking performed to complement the change
+- Potentially insecure code
+
+Do not add these comments directly to the source code, unless the
+reviewer requires you to do so.
+
+This
+[saves reviewers time and helps authors catch mistakes earlier](https://www.ibm.com/developerworks/rational/library/11-proven-practices-for-peer-review/index.html#__RefHeading__97_174136755).
+
### The responsibility of the maintainer
Maintainers are responsible for the overall health, quality, and consistency of
@@ -85,8 +117,8 @@ Since a maintainer's job only depends on their knowledge of the overall GitLab
codebase, and not that of any specific domain, they can review, approve and merge
merge requests from any team and in any product area.
-In fact, authors are recommended to get their merge requests merged by maintainers
-from other teams than their own, to ensure that all code across GitLab is consistent
+In fact, authors are encouraged to get their merge requests merged by maintainers
+from teams other than their own, to ensure that all code across GitLab is consistent
and can be easily understood by all contributors, from both inside and outside the
company, without requiring team-specific expertise.
@@ -103,6 +135,18 @@ as the maintainer to ultimately approve and merge it.
Maintainers should check before merging if the merge request is approved by the
required approvers.
+Maintainers must check before merging if the merge request is introducing new
+vulnerabilities, by inspecting the list in the Merge Request [Security
+Widget](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate).
+When in doubt, a [Security Engineer][team] can be involved. The list of detected
+vulnerabilities must be either empty or containing:
+
+- dismissed vulnerabilities in case of false positives
+- vulnerabilities converted to issues
+
+Maintainers should **never** dismiss vulnerabilities to "empty" the list,
+without duly verifying them.
+
## Best practices
### Everyone
@@ -153,7 +197,7 @@ first time.
### Assigning a merge request for a review
-If you want to have your merge request reviewed you can assign it to any reviewer. The list of reviewers can be found on [Engineering projects](https://about.gitlab.com/handbook/engineering/projects/) page.
+If you want to have your merge request reviewed, you can assign it to any reviewer. The list of reviewers can be found on [Engineering projects](https://about.gitlab.com/handbook/engineering/projects/) page.
You can also use `ready for review` label. That means that your merge request is ready to be reviewed and any reviewer can pick it. It is recommended to use that label only if there isn't time pressure and make sure the merge request is assigned to a reviewer.
@@ -189,6 +233,9 @@ experience, refactors the existing code). Then:
subsequent revisions for anything that would be spotted after that.
- Consider using the [Squash and
merge][squash-and-merge] feature when the merge request has a lot of commits.
+ When merging code a maintainer should only use the squash feature if the
+ author has already set this option or if the merge request clearly contains a
+ messy commit history that is intended to be squashed.
[squash-and-merge]: https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#squash-and-merge
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index b7990e1b558..55aed023325 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -368,6 +368,16 @@ 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.
+
## General Documentation vs Technical Articles
### General documentation
@@ -552,6 +562,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
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index a227e2f6640..790b1bf951b 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -511,7 +511,7 @@ module EE
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
# ...
end
end
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
new file mode 100644
index 00000000000..f55f01720fd
--- /dev/null
+++ b/doc/development/fe_guide/graphql.md
@@ -0,0 +1,83 @@
+# GraphQL
+
+We use [Apollo] and [Vue Apollo][vue-apollo] for working with GraphQL
+on the frontend.
+
+In order to use GraphQL, you need to enable the `graphql` feature flag,
+read more about [Feature Flags][feature-flags].
+
+## Apollo Client
+
+To save duplicated clients getting created in different apps, we have a
+[default client][defualt-client] that should be used. This setups the
+Apollo client with the correct URL and also sets the CSRF headers.
+
+## GraphQL Queries
+
+To save query compilation at runtime, webpack can directly import `.graphql`
+files. This allows webpack to preprocess the query at compile time instead
+of the client doing compilation of queries.
+
+## Usage in Vue
+
+To use Vue Apollo, import the [Vue Apollo][vue-apollo] plugin as well
+as the default client. This should be created at the same point
+the Vue application is mounted.
+
+```javascript
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import defaultClient from '~/lib/graphql';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
+new Vue({
+ ...,
+ apolloProvider,
+ ...
+});
+```
+
+Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation][vue-apollo-docs].
+
+### Testing
+
+With [Vue test utils][vue-test-utils] it is easy to quickly test components that
+fetch GraphQL queries. The simplest way is to use `shallowMount` and then set
+the data on the component
+
+```javascript
+it('tests apollo component', () => {
+ const vm = shallowMount(App);
+
+ vm.setData({
+ ...mock data
+ });
+});
+```
+
+## Usage outside of Vue
+
+It is also possible to use GraphQL outside of Vue by directly importing
+and using the default client with queries.
+
+```javascript
+import defaultClient from '~/lib/graphql';
+import query from './query.graphql';
+
+defaultClient.query(query)
+ .then(result => console.log(result));
+```
+
+Read more about the [Apollo] client in the [Apollo documentation][apollo-client-docs].
+
+[Apollo]: https://www.apollographql.com/
+[vue-apollo]: https://github.com/Akryum/vue-apollo/
+[vue-apollo-docs]: https://akryum.github.io/vue-apollo/
+[feature-flags]: ../feature_flags.md
+[default-client]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/lib/graphql.js
+[apollo-client-docs]: https://www.apollographql.com/docs/tutorial/client.html
+[vue-test-utils]: https://vue-test-utils.vuejs.org/
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 11b9a2e6a64..cca3ad6fae6 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -54,6 +54,9 @@ Vuex specific design patterns and practices.
## [Axios](axios.md)
Axios specific practices and gotchas.
+## [GraphQL](graphql.md)
+How to use GraphQL
+
## [Icons and Illustrations](icons.md)
How we use SVG for our Icons and Illustrations.
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/github_importer.md b/doc/development/github_importer.md
index e860bde48dc..863ac049db6 100644
--- a/doc/development/github_importer.md
+++ b/doc/development/github_importer.md
@@ -131,7 +131,7 @@ our import as failed because of this.
To prevent this from happening we periodically refresh the expiration time of
the import process. This works by storing the JID of the import job in the
database, then refreshing this JID's TTL at various stages throughout the import
-process. This is done by calling `Project#refresh_import_jid_expiration`. By
+process. This is done by calling `ProjectImportState#refresh_jid_expiration`. By
refreshing this TTL we can ensure our import does not get marked as failed so
long we're still performing work.
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 84dea7ce9aa..a5a34d82bec 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -92,7 +92,7 @@ describe API::Labels do
end
```
-## Avoid using `allow_any_instance_of` in RSpec
+## Avoid using `expect_any_instance_of` or `allow_any_instance_of` in RSpec
### Why
@@ -102,7 +102,7 @@ end
error like this:
```
- 1.1) Failure/Error: allow_any_instance_of(ApplicationSetting).to receive_messages(messages)
+ 1.1) Failure/Error: expect_any_instance_of(ApplicationSetting).to receive_messages(messages)
Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.
```
@@ -112,7 +112,7 @@ Instead of writing:
```ruby
# Don't do this:
-allow_any_instance_of(Project).to receive(:add_import_job)
+expect_any_instance_of(Project).to receive(:add_import_job)
```
We could write:
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index f75f5882b56..8f1317e235d 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -43,7 +43,7 @@ are very appreciative of the work done by translators and proofreaders!
- Greek
- Proofreaders needed.
- Hebrew
- - Proofreaders needed.
+ - Yaron Shahrabani - [GitLab](https://gitlab.com/yarons), [Crowdin](https://crowdin.com/profile/YaronSh)
- Hungarian
- Proofreaders needed.
- Indonesian
@@ -57,12 +57,14 @@ are very appreciative of the work done by translators and proofreaders!
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie)
+ - Jeongwhan Choi - [GitLab](https://gitlab.com/jeongwhanchoi), [Crowdin](https://crowdin.com/profile/jeongwhanchoi)
- Mongolian
- Proofreaders needed.
- Norwegian Bokmal
- Proofreaders needed.
- Polish
- Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
+ - Maksymilian Roman - [GitLab](https://gitlab.com/villaincandle), [Crowdin](https://crowdin.com/profile/villaincandle)
- Portuguese
- Proofreaders needed.
- Portuguese, Brazilian
diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md
index 082acbedcd2..0d98180add0 100644
--- a/doc/development/new_fe_guide/development/testing.md
+++ b/doc/development/new_fe_guide/development/testing.md
@@ -6,9 +6,15 @@ Tests relevant for frontend development can be found at two places:
- [frontend unit tests](#frontend-unit-tests)
- [frontend component tests](#frontend-component-tests)
- [frontend integration tests](#frontend-integration-tests)
+- `spec/frontend/` which are run by Jest and contain
+ - [frontend unit tests](#frontend-unit-tests)
+ - [frontend component tests](#frontend-component-tests)
+ - [frontend integration tests](#frontend-integration-tests)
- `spec/features/` which are run by RSpec and contain
- [feature tests](#feature-tests)
+All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#53757]).
+
In addition there were feature tests in `features/` run by Spinach in the past.
These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
@@ -17,6 +23,8 @@ See also:
- [old testing guide](../../testing_guide/frontend_testing.html)
- [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components)
+[#53757]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53757
+
## Frontend unit tests
Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
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/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 1830641431e..309babb5f94 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -7,31 +7,36 @@ Review Apps are automatically deployed by each pipeline, both in
## How does it work?
1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
- [`review` job][review-job] is automatically started.
-1. The `review` job [triggers a pipeline][cng-pipeline] in the
- [`CNG-mirror`][cng-mirror] project.
- - We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative
- **G**itLab), project's registry is not overloaded with a lot of transient
- Docker images.
-1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g.
- `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) based on the commit from the
- [GitLab pipeline][gitlab-pipeline] and store them in its
- [registry][cng-mirror-registry].
-1. Once all images are built, the Review App is deployed using
- [the official GitLab Helm chart][helm-chart] to the
- [`review-apps-ee` Kubernetes cluster on GCP][review-apps-ee]
- - The actual scripts used to deploy the Review App can be found at
- [`scripts/review_apps/review-apps.sh`][review-apps.sh]
- - These scripts are basically
- [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
- default CNG images are overridden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry].
- - Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get a dedicated environment for your branch that's very close to what it
- would look in production.
-1. Once the `review` job succeeds, you should be able to use your Review App
- thanks to the direct link to it from the MR widget. The default username is
- `root` and its password can be found in the 1Password secure note named
+ [`review-deploy`][review-deploy-job] job is automatically started.
+1. The `review-deploy` job:
+ 1. Waits for the `gitlab:assets:compile` job to finish since the
+ [`CNG-mirror`][cng-mirror] pipeline triggerred in the following step
+ depends on it.
+ 1. [Triggers a pipeline][cng-pipeline] in the [`CNG-mirror`][cng-mirror]
+ project.
+ - We use the `CNG-mirror` project so that the `CNG`, (**C**loud
+ **N**ative **G**itLab), project's registry is not overloaded with a
+ lot of transient Docker images.
+ - The `CNG-mirror` pipeline creates the Docker images of each component
+ (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) based on the
+ commit from the [GitLab pipeline][gitlab-pipeline] and store them in
+ its [registry][cng-mirror-registry].
+ 1. Once all images are built by [`CNG-mirror`][cng-mirror], the Review App
+ is deployed using [the official GitLab Helm chart][helm-chart] to the
+ [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee]
+ Kubernetes cluster on GCP.
+ - The actual scripts used to deploy the Review App can be found at
+ [`scripts/review_apps/review-apps.sh`][review-apps.sh].
+ - These scripts are basically
+ [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
+ default CNG images are overridden with the images built and stored in the
+ [`CNG-mirror` project's registry][cng-mirror-registry].
+ - Since we're using [the official GitLab Helm chart][helm-chart], this means
+ you get a dedicated environment for your branch that's very close to what
+ it would look in production.
+1. Once the `review-deploy` job succeeds, you should be able to use your Review
+ App thanks to the direct link to it from the MR widget. The default username
+ is `root` and its password can be found in the 1Password secure note named
**gitlab-{ce,ee} Review App's root password** (note that there's currently
[a bug where the default password seems to be overridden][password-bug]).
@@ -39,16 +44,58 @@ Review Apps are automatically deployed by each pipeline, both in
- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
[GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
- allows to have a link to the Review App directly from the merge request widget.
-- The manual `stop_review` in the `test` stage can be used to stop a Review App
- manually, and is also started by GitLab once a branch is deleted.
-- Review Apps are cleaned up regularly using a pipeline schedule that runs
- the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script.
+ allows to have a link to the Review App directly from the merge request
+ widget.
- If the Review App deployment fails, you can simply retry it (there's no need
- to run the `stop_review` job first).
-- If you're unable to log in using the `root` username and password, you may
- encounter [this bug][password-bug]. Stop the Review App via the `stop_review`
- manual job and then retry the `review` job to redeploy the Review App.
+ to run the [`review-stop`][gitlab-ci-yml] job first).
+- The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to
+ stop a Review App manually, and is also started by GitLab once a branch is
+ deleted.
+- Review Apps are cleaned up regularly using a pipeline schedule that runs
+ the [`schedule:review-cleanup`][gitlab-ci-yml] job.
+
+## QA runs
+
+On every [pipeline][gitlab-pipeline] during the `test` stage, the
+`review-qa-smoke` job is automatically started: it runs the smoke QA suite.
+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
@@ -74,15 +121,17 @@ find a way to limit it to only us.**
> This isn't enabled for forks.
[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/35850709
-[review-job]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/118076368
+[review-deploy-job]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/118076368
[cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror
[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/35883435
[cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry
[helm-chart]: https://gitlab.com/charts/gitlab/
+[review-apps-ce]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps
[review-apps-ee]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps
[review-apps.sh]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/review_apps/review-apps.sh
[automated_cleanup.rb]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/review_apps/automated_cleanup.rb
[Auto-DevOps.gitlab-ci.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+[gitlab-ci-yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml
[gitlab-k8s-integration]: https://docs.gitlab.com/ee/user/project/clusters/index.html
[password-bug]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53621
diff --git a/doc/install/docker.md b/doc/install/docker.md
index c7dc9db70c5..e90f6645b0c 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -7,9 +7,10 @@ GitLab provides official Docker images to allowing you to easily take advantage
## Omnibus GitLab based images
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/installation.md b/doc/install/installation.md
index 06293b8caf5..d041bfa863a 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,7 +12,7 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-5-stable`).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-6-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
@@ -300,9 +300,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-5-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-6-stable gitlab
-**Note:** You can change `11-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 18c9cd116f1..a63656fafef 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -604,6 +604,8 @@ If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
+You may also want to restore any TLS keys, certificates, or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+
Depending on your case, you might want to run the restore command with one or
more of the following options:
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/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index b770f2544d2..4b65b901487 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
-[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md)
+[Two-factor Authentication (2FA)](../user/profile/account/two_factor_authentication.md)
## Enforcing 2FA for all users
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 394f3ea60b7..766a23c419d 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -44,6 +44,5 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth)
- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin)
-- [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 6bb2e236dc1..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
@@ -479,14 +486,23 @@ no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again.
+#### Migrations
+
> [Introduced][ce-21955] in GitLab 11.4
Database initialization and migrations for PostgreSQL can be configured to run
within the application pod by setting the project variables `DB_INITIALIZE` and
`DB_MIGRATE` respectively.
-If present, `DB_INITIALIZE` will be run as a shell command within an application pod as a helm
-post-install hook. Note that this means that if any deploy succeeds,
+If present, `DB_INITIALIZE` will be run as a shell command within an
+application pod as a helm post-install hook. As some applications will
+not run without a successful database initialization step, GitLab will
+deploy the first release without the application deployment and only the
+database initialization step. After the database initialization completes,
+GitLab will deploy a second release with the application deployment as
+normal.
+
+Note that a post-install hook means that if any deploy succeeds,
`DB_INITIALIZE` will not be processed thereafter.
If present, `DB_MIGRATE` will be run as a shell command within an application pod as
@@ -657,8 +673,6 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. |
| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. |
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
-| `OLD_REPORTS_DISABLED` | From GitLab 11.5, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
-| `NEW_REPORTS_DISABLED` | From GitLab 11.5, this variable can be used to disable the `sast_dashboard` job. If the variable is present, the job will not be created. |
TIP: **Tip:**
Set up the replica variables using a
diff --git a/doc/update/11.5-to-11.6.md b/doc/update/11.5-to-11.6.md
new file mode 100644
index 00000000000..031abc434ca
--- /dev/null
+++ b/doc/update/11.5-to-11.6.md
@@ -0,0 +1,390 @@
+---
+comments: false
+---
+
+# From 11.5 to 11.6
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-6-stable`. You can select the branch in the version
+dropdown at the top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz
+echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz
+cd ruby-2.5.3
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go
+1.9.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://dl.google.com/go/go1.10.5.linux-amd64.tar.gz
+echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.5.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-6-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-6-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update gitlab-pages
+
+#### Only needed if you use GitLab Pages
+
+Install and compile gitlab-pages. GitLab-Pages uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-pages
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
+sudo -u git -H make
+```
+
+### 11. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 12. Update configuration files
+
+#### New `unicorn.rb` configuration
+
+We have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future.
+
+Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-stable/config/unicorn.rb.example but with your settings.
+In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below:
+
+```ruby
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+
+before_exec do |server|
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do |server, worker|
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+after_fork do |server, worker|
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+```
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-5-stable:config/gitlab.yml.example origin/11-6-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/11-5-stable:lib/support/nginx/gitlab-ssl origin/11-6-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/11-5-stable:lib/support/nginx/gitlab origin/11-6-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-5-stable:lib/support/init.d/gitlab.default.example origin/11-6-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 13. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 14. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 15. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (11.5)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 11.4 to 11.5](11.4-to-11.5.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md
new file mode 100644
index 00000000000..5afbf9f2934
--- /dev/null
+++ b/doc/user/admin_area/custom_project_templates.md
@@ -0,0 +1,25 @@
+# Custom instance-level project templates **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2.
+
+When you create a new project, creating it based on custom project templates is
+a convenient option to bootstrap from an existing project boilerplate.
+The administration setting to configure a GitLab group that serves as template
+source can be found under **Admin > Settings > Custom project templates**.
+
+Within this section, you can configure the group where all the custom project
+templates are sourced. Every project directly under the group namespace will be
+available to the user if they have access to them. For example, every public
+project in the group will be available to every logged user. However,
+private projects will be available only if the user has view [permissions](../permissions.md)
+in the project:
+
+- Project Owner, Maintainer, Developer, Reporter or Guest
+- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest
+
+Projects below subgroups of the template group are **not** supported.
+
+Repository and database information that are copied over to each new project are
+identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
+
+If you would like to set project templates at a group level, please see [Custom group-level project templates](../group/custom_project_templates.md). \ No newline at end of file
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/custom_project_templates.md b/doc/user/group/custom_project_templates.md
new file mode 100644
index 00000000000..eaf0273050b
--- /dev/null
+++ b/doc/user/group/custom_project_templates.md
@@ -0,0 +1,23 @@
+# Custom group-level project templates **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
+
+When you create a new project, creating it based on custom project templates is
+a convenient option to bootstrap from an existing project boilerplate.
+The group-level setting to configure a GitLab group that serves as template
+source can be found under **Group > Settings > General > Custom project templates**.
+
+Within this section, you can configure the group where all the custom project
+templates are sourced. Every project directly under the group namespace will be
+available to the user if they have access to them. For example, every public
+project in the group will be available to every logged in user. However,
+private projects will be available only if the user has view [permissions](../permissions.md)
+in the project. That is, users with Owner, Maintainer, Developer, Reporter or Guest roles for projects,
+or for groups to which the project belongs.
+
+Projects of nested subgroups of a selected template source cannot be used.
+
+Repository and database information that are copied over to each new project are
+identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
+
+If you would like to set project templates at an instance level, please see [Custom instance-level project templates](../admin_area/custom_project_templates.md). \ No newline at end of file
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index d673fa4d21a..5fea683a7fd 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -259,11 +259,17 @@ types with every project in a group.
Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium).
+#### Group-level project templates **[PREMIUM]**
+
+Define project templates at a group-level by setting a group as a template source.
+[Learn more about group-level project templates](custom_project_templates.md).
+
### Advanced settings
- **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/markdown.md b/doc/user/markdown.md
index 6c6119a2691..debebd4c081 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1022,7 +1022,7 @@ A link starting with a `/` is relative to the wiki root.
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
-[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
+[katex-subset]: https://katex.org/docs/supported.html "Macros supported by KaTeX"
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
[commonmarker]: https://github.com/gjtorikian/commonmarker
[commonmark-spec]: https://spec.commonmark.org/current/
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 6735710e2bb..6d05e2feeec 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:**
@@ -92,13 +97,47 @@ To add an existing Kubernetes cluster to your project:
the `ca.crt` contents here.
- **Token** -
GitLab authenticates against Kubernetes using service tokens, which are
- scoped to a particular `namespace`. If you don't have a service token yet,
- you can follow the
- [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
- to create one. You can also view or create service tokens in the
- [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/)
- (under **Config > Secrets**). **The account that will issue the service token
- must have admin privileges on the cluster.**
+ scoped to a particular `namespace`.
+ **The token used should belong to a service account with
+ [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+ privileges.** To create this service account:
+
+ 1. Create a `gitlab` service account in the `default` namespace:
+
+ ```bash
+ kubectl create -f - <<EOF
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: gitlab
+ namespace: default
+ EOF
+ ```
+ 1. Create a cluster role binding to give the `gitlab` service account
+ `cluster-admin` privileges:
+
+ ```bash
+ kubectl create -f - <<EOF
+ kind: ClusterRoleBinding
+ apiVersion: rbac.authorization.k8s.io/v1
+ metadata:
+ name: gitlab-cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: gitlab
+ namespace: default
+ roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+ EOF
+ ```
+ NOTE: **Note:**
+ For GKE clusters, you will need the
+ `container.clusterRoleBindings.create` permission to create a cluster
+ role binding. You can follow the [Google Cloud
+ documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
+ to grant access.
- **Project namespace** (optional) - You don't have to fill it in; by leaving
it blank, GitLab will create one for you. Also:
- Each project should have a unique namespace.
@@ -142,8 +181,9 @@ Whether ABAC or RBAC is enabled, GitLab will create the necessary
service accounts and privileges in order to install and run
[GitLab managed applications](#installing-applications):
-- A `gitlab` service account with `cluster-admin` privileges will be created in the
- `default` namespace, which will be used by GitLab to manage the newly created cluster.
+- If GitLab is creating the cluster, a `gitlab` service account with
+ `cluster-admin` privileges will be created in the `default` namespace,
+ which will be used by GitLab to manage the newly created cluster.
- A project service account with [`edit`
privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
@@ -210,16 +250,18 @@ 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 |
| ----------- | :------------: | ----------- | --------------- |
@@ -238,13 +280,10 @@ by GitLab before installing any of the above applications.
## Getting the external IP address
NOTE: **Note:**
-You need a load balancer installed in your cluster in order to obtain the
-external IP address with the following procedure. It can be deployed using the
-[**Ingress** application](#installing-applications).
-
-NOTE: **Note:**
-Knative will include its own load balancer in the form of [Istio](https://istio.io).
-At this time, to determine the external IP address, you will need to follow the manual approach.
+With the following procedure, a load balancer must be installed in your cluster
+to obtain the external IP address. You can use either
+[Ingress](#installing-applications), or Knative's own load balancer
+([Istio](https://istio.io)) if using [Knative](#installing-applications).
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
@@ -253,7 +292,7 @@ address associated to your load balancer.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6.
-If you installed the Ingress [via the **Applications**](#installing-applications),
+If you [installed Ingress or Knative](#installing-applications),
you should see the Ingress IP address on this same page within a few minutes.
If you don't see this, GitLab might not be able to determine the IP address of
your ingress application in which case you should manually determine it.
@@ -315,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
@@ -400,12 +435,34 @@ GitLab CI/CD build environment.
| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
-| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
+| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
NOTE: **NOTE:**
Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
service account of the cluster integration.
+### Troubleshooting missing `KUBECONFIG` or `KUBE_TOKEN`
+
+GitLab will create a new service account specifically for your CI builds. The
+new service account is created when the cluster is added to the project.
+Sometimes there may be errors that cause the service account creation to fail.
+
+In such instances, your build will not be passed the `KUBECONFIG` or
+`KUBE_TOKEN` variables and, if you are using Auto DevOps, your Auto DevOps
+pipelines will no longer trigger a `production` deploy build. You will need to
+check the [logs](../../../administration/logs.md) to debug why the service
+account creation failed.
+
+A common reason for failure is that the token you gave GitLab did not have
+[`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+privileges as GitLab expects.
+
+Another common problem for why these variables are not being passed to your
+builds is that they must have a matching
+[`environment:name`](../../../ci/environments.md#defining-environments). If
+your build has no `environment:name` set, it will not be passed the Kubernetes
+credentials.
+
## Enabling or disabling the Kubernetes cluster integration
After you have successfully added your cluster information, you can enable the
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/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md
index dc985e87a96..ebf87890cfd 100644
--- a/doc/user/project/import/bitbucket_server.md
+++ b/doc/user/project/import/bitbucket_server.md
@@ -27,7 +27,7 @@ request.
1. Bitbucket Server allows multiple levels of threading. GitLab
import will collapse this into one discussion and quote part of the original
comment.
-1. Declined pull requests have unrecahable commits, which prevents the GitLab
+1. Declined pull requests have unreachable commits, which prevents the GitLab
importer from generating a proper diff. These pull requests will show up as
empty changes.
1. Attachments in Markdown are currently not imported.
diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md
index b050c83fb72..df320de7e16 100644
--- a/doc/user/project/integrations/emails_on_push.md
+++ b/doc/user/project/integrations/emails_on_push.md
@@ -1,20 +1,20 @@
# Enabling emails on push
-By enabling this service, you will be able to receive email notifications for
-every change that is pushed to your project.
+By enabling this service, you will receive email notifications for every change
+that is pushed to your project.
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Emails on push** service to configure it.
+From the [Integrations page](project_services.md#accessing-the-project-services)
+select **Emails on push** service to activate and configure it.
In the _Recipients_ area, provide a list of emails separated by spaces or newlines.
-You can configure any of the following settings depending on your preference.
+The following options are available:
+ **Push events** - Email will be triggered when a push event is received
+ **Tag push events** - Email will be triggered when a tag is created and pushed
+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
----
-
-![Email on push service settings](img/emails_on_push_service.png)
+| Settings | Notification |
+| --- | --- |
+| ![Email on push service settings](img/emails_on_push_service.png) | ![Email on push notification](img/emails_on_push_email.png) |
diff --git a/doc/user/project/integrations/img/emails_on_push_email.png b/doc/user/project/integrations/img/emails_on_push_email.png
new file mode 100644
index 00000000000..406eeb18410
--- /dev/null
+++ b/doc/user/project/integrations/img/emails_on_push_email.png
Binary files differ
diff --git a/doc/user/project/integrations/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png
index df301aa1eeb..84e42eca9a2 100644
--- a/doc/user/project/integrations/img/emails_on_push_service.png
+++ b/doc/user/project/integrations/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_widget.png b/doc/user/project/merge_requests/img/merge_request_widget.png
index 6c2317b29b5..58562fcb034 100644
--- a/doc/user/project/merge_requests/img/merge_request_widget.png
+++ b/doc/user/project/merge_requests/img/merge_request_widget.png
Binary files differ
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/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index ecbc8534eea..ccef2853e3f 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -1,15 +1,31 @@
-# Merge conflict resolution
+# Merge request conflict resolution
-> [Introduced][ce-5479] in GitLab 8.11.
+Merge conflicts occur when two branches have different changes that cannot be
+merged automatically.
-When a merge request has conflicts, GitLab may provide the option to resolve
-those conflicts in the GitLab UI. (See
-[conflicts available for resolution](#conflicts-available-for-resolution) for
-more information on when this is available.) If this is an option, you will see
-a **resolve these conflicts** link in the merge request widget:
+Git is able to automatically merge changes between branches in most cases, but
+there are situations where Git will require your assistance to resolve the
+conflicts manually. Typically, this is necessary when people change the same
+parts of the same files.
+
+GitLab will prevent merge requests from being merged until all conflicts are
+resolved. Conflicts can be resolved locally, or in many cases within GitLab
+(see [conflicts available for resolution](#conflicts-available-for-resolution)
+for information on when this is available).
![Merge request widget](img/merge_request_widget.png)
+NOTE: **Note:**
+GitLab resolves conflicts by creating a merge commit in the source branch that
+is not automatically merged into the target branch. This allows the merge
+commit to be reviewed and tested before the changes are merged, preventing
+unintended changes entering the target branch without review or breaking the
+build.
+
+## Resolve conflicts: interactive mode
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479) in GitLab 8.11.
+
Clicking this will show a list of files with conflicts, with conflict sections
highlighted:
@@ -21,9 +37,9 @@ request into the source branch, resolving the conflicts using the options
chosen. If the source branch is `feature` and the target branch is `master`,
this is similar to performing `git checkout feature; git merge master` locally.
-## Merge conflict editor
+## Resolve conflicts: inline editor
-> Introduced in GitLab 8.13.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6374) in GitLab 8.13.
The merge conflict resolution editor allows for more complex merge conflicts,
which require the user to manually modify a file in order to resolve a conflict,
@@ -50,5 +66,3 @@ Additionally, GitLab does not detect conflicts in renames away from a path. For
example, this will not create a conflict: on branch `a`, doing `git mv file1
file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
present in the branch after the merge request is merged.
-
-[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 9daacc37994..051277dfe02 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -83,12 +83,12 @@ The next time a pipeline is scheduled, your credentials will be used.
![Schedules list](img/pipeline_schedules_ownership.png)
-> **Note:**
-When the owner of the schedule doesn't have the ability to create pipelines
-anymore, due to e.g., being blocked or removed from the project, or lacking
-the permission to run on protected branches or tags. When this happened, the
-schedule is deactivated. Another user can take ownership and activate it, so
-the schedule can be run again.
+NOTE: **Note:**
+If the owner of a pipeline schedule doesn't have the ability to create pipelines
+on the target branch, the schedule will stop creating new pipelines. This can
+happen if, for example, the owner is blocked or removed from the project, or
+the target branch or tag is protected. In this case, someone with sufficient
+privileges must take ownership of the schedule.
## Advanced admin configuration
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/workflow/notifications.md b/doc/workflow/notifications.md
index c590ac4b0ba..020aa73f809 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -64,6 +64,8 @@ Below is the table of events users can be notified of:
|------------------------------|-------------------------------------------------------------------|------------------------------|
| New SSH key added | User | Security email, always sent. |
| New email added | User | Security email, always sent. |
+| Email changed | User | Security email, always sent. |
+| Password changed | User | Security email, always sent. |
| New user created | User | Sent on user creation, except for omniauth (LDAP)|
| User added to project | User | Sent when user is added to project |
| Project access level changed | User | Sent when user project access level is changed |
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index cecff6d3b81..ee8dc822098 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 449faf5f8da..8abb24e6f69 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -7,8 +7,8 @@ module API
LOG_FILENAME = Rails.root.join("log", "api_json.log")
NO_SLASH_URL_PART_REGEX = %r{[^/]+}
- PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
- COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
+ NAMESPACE_OR_PROJECT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
+ COMMIT_ENDPOINT_REQUIREMENTS = NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
@@ -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/api_guard.rb b/lib/api/api_guard.rb
index 61357b3f1d6..af9b519ed9e 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -94,6 +94,7 @@ module API
Gitlab::Auth::TokenNotFoundError,
Gitlab::Auth::ExpiredError,
Gitlab::Auth::RevokedError,
+ Gitlab::Auth::ImpersonationDisabled,
Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
@@ -121,6 +122,11 @@ module API
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
+ when Gitlab::Auth::ImpersonationDisabled
+ Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
+ :invalid_token,
+ "Token is an impersonation token but impersonation was disabled.")
+
when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index c2abf9155f3..a1851ba3627 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
AWARDABLES.each do |awardable_params|
awardable_string = awardable_params[:type].pluralize
awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index ab670988f47..ba554e00a16 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -22,7 +22,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{source_type}"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Gets a list of #{source_type} badges viewable by the authenticated user." do
detail 'This feature was introduced in GitLab 10.6.'
success Entities::Badge
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index c80e1c57864..b7c77730afb 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/boards' do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 2735d410c8e..e7e58ad0e66 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -6,7 +6,7 @@ module API
class Branches < Grape::API
include PaginationParams
- BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
+ BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :download_code, user_project }
@@ -20,7 +20,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository branches' do
success Entities::Branch
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 99553d993ca..62c966e06b4 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include PaginationParams
before { authenticate! }
@@ -29,7 +29,7 @@ module API
not_found!('Commit') unless user_project.commit(params[:sha])
- pipelines = user_project.pipelines.where(sha: params[:sha])
+ pipelines = user_project.ci_pipelines.where(sha: params[:sha])
statuses = ::CommitStatus.where(pipeline: pipelines)
statuses = statuses.latest unless to_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
@@ -75,7 +75,7 @@ module API
pipeline = @project.pipeline_for(ref, commit.sha)
unless pipeline
- pipeline = @project.pipelines.create!(
+ pipeline = @project.ci_pipelines.create!(
source: :external,
sha: commit.sha,
ref: ref,
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 337b92a6183..9d23daafe95 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -23,7 +23,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository commits' do
success Entities::Commit
end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index ce35720d408..df6d2721977 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -31,7 +31,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authorize_admin_project }
desc "Get a specific project's deploy keys" do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 6747e2e5005..8706a971a1a 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 39c6d28391d..91eb6a23701 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -17,7 +17,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
success Entities::Discussion
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index cff05643f3b..5dbfbb85e9e 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -145,7 +145,9 @@ module API
expose :import_status
# TODO: Use `expose_nil` once we upgrade the grape-entity gem
- expose :import_error, if: lambda { |status, _ops| status.import_error }
+ expose :import_error, if: lambda { |project, _ops| project.import_state&.last_error } do |project|
+ project.import_state.last_error
+ end
end
class BasicProjectDetails < ProjectIdentity
@@ -248,7 +250,10 @@ module API
expose :creator_id
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? }
expose :import_status
- expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] }
+
+ expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project|
+ project.import_state&.last_error
+ end
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
@@ -710,6 +715,10 @@ module API
expose :diff_refs, using: Entities::DiffRefs
+ # Allow the status of a rebase to be determined
+ expose :merge_error
+ expose :rebase_in_progress?, as: :rebase_in_progress, if: -> (_, options) { options[:include_rebase_in_progress] }
+
expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] }
def build_available?(options)
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index c64217a6977..633f24d3c9a 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
diff --git a/lib/api/events.rb b/lib/api/events.rb
index 6e0b508be19..44dae57770d 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -97,7 +97,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "List a Project's visible events" do
success Entities::Event
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index bcd2cd48a45..ca59d330e1c 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -2,7 +2,9 @@
module API
class Files < Grape::API
- FILE_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
+ include APIGuard
+
+ FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
# Prevents returning plain/text responses for files with .txt extension
after_validation { content_type "application/json" }
@@ -79,6 +81,8 @@ module API
requires :id, type: String, desc: 'The project ID'
end
resource :projects, requirements: FILE_ENDPOINT_REQUIREMENTS do
+ allow_access_with_scope :read_repository, if: -> (request) { request.get? || request.head? }
+
desc 'Get raw file metadata from repository'
params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index dc30e868e2e..9a20ee8c8b9 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -19,7 +19,7 @@ module API
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/boards' do
desc 'Find a group board' do
detail 'This feature was introduced in 10.6'
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index b36436dbf43..d4287e4a7c4 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group milestones' do
success Entities::Milestone
end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index ae7241e9a30..3f048e0dc56 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -11,7 +11,7 @@ module API
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get group-level variables' do
success Entities::Variable
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index b3d10721692..626a2008dee 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -140,7 +140,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
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/issues.rb b/lib/api/issues.rb
index 491b5085bb8..dac700482b4 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -101,7 +101,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group issues' do
success Entities::IssueBasic
end
@@ -128,7 +128,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 2229cbcd9d4..7c2d8ff11bf 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Download the artifacts archive from a job' do
detail 'This feature was introduced in GitLab 8.10'
end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 697555c9605..80a5cbd6b19 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
@@ -56,7 +56,7 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/pipelines/:pipeline_id/jobs' do
- pipeline = user_project.pipelines.find(params[:pipeline_id])
+ pipeline = user_project.ci_pipelines.find(params[:pipeline_id])
builds = pipeline.builds
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 28555454307..2e676b0aa6b 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all labels of the project' do
success Entities::Label
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index a8f67be3463..461ffe71a62 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index e4fb890960a..6ad30aa56e0 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::MergeRequestDiff
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 16f07f16387..8c1951cc535 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -74,6 +74,19 @@ module API
options
end
+ def authorize_push_to_merge_request!(merge_request)
+ forbidden!('Source branch does not exist') unless
+ merge_request.source_branch_exists?
+
+ user_access = Gitlab::UserAccess.new(
+ current_user,
+ project: merge_request.source_project
+ )
+
+ forbidden!('Cannot push to source branch') unless
+ user_access.can_push_to_branch?(merge_request.source_branch)
+ end
+
params :merge_requests_params do
optional :state, type: String, values: %w[opened closed locked merged all], default: 'all',
desc: 'Return opened, closed, locked, merged, or all merge requests'
@@ -122,7 +135,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group merge requests' do
success Entities::MergeRequestBasic
end
@@ -141,7 +154,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include TimeTrackingEndpoints
helpers do
@@ -239,6 +252,7 @@ module API
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML'
optional :include_diverged_commits_count, type: Boolean, desc: 'Returns the commits count behind the target branch'
+ optional :include_rebase_in_progress, type: Boolean, desc: 'Returns whether a rebase operation is ongoing '
end
desc 'Get a single merge request' do
success Entities::MergeRequest
@@ -246,7 +260,13 @@ module API
get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html], include_diverged_commits_count: params[:include_diverged_commits_count]
+ present merge_request,
+ with: Entities::MergeRequest,
+ current_user: current_user,
+ project: user_project,
+ render_html: params[:render_html],
+ include_diverged_commits_count: params[:include_diverged_commits_count],
+ include_rebase_in_progress: params[:include_rebase_in_progress]
end
desc 'Get the participants of a merge request' do
@@ -378,6 +398,19 @@ module API
.cancel(merge_request)
end
+ desc 'Rebase the merge request against its target branch' do
+ detail 'This feature was added in GitLab 11.6'
+ end
+ put ':id/merge_requests/:merge_request_iid/rebase' do
+ merge_request = find_project_merge_request(params[:merge_request_iid])
+
+ authorize_push_to_merge_request!(merge_request)
+
+ RebaseWorker.perform_async(merge_request.id, current_user.id)
+
+ status :accepted
+ end
+
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 76639fbb031..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
@@ -28,7 +43,7 @@ module API
params do
requires :id, type: String, desc: "Namespace's ID or path"
end
- get ':id' do
+ get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
present user_namespace, with: Entities::Namespace, current_user: current_user
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 9f323b87baf..1bdf7aeb119 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
noteables_str = noteable_type.to_s.underscore.pluralize
desc "Get a list of #{noteable_type.to_s.downcase} notes" do
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index 4d9a4629268..8cb46bd3ad6 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index c9ad47e0f0d..78442f465bd 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -4,7 +4,7 @@ module API
class PagesDomains < Grape::API
include PaginationParams
- PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
+ PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
before do
authenticate!
@@ -54,7 +54,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
require_pages_enabled!
end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
index ed0a38b9d70..47b711917e2 100644
--- a/lib/api/pipeline_schedules.rb
+++ b/lib/api/pipeline_schedules.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all pipeline schedules' do
success Entities::PipelineSchedule
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index cba1e3a6684..7a7b23d2bbb 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::PipelineBasic
@@ -130,7 +130,7 @@ module API
helpers do
def pipeline
- @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ @pipeline ||= user_project.ci_pipelines.find(params[:pipeline_id])
end
end
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 4af4c6ac593..0e7576c9243 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -29,7 +29,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project hooks' do
success Entities::ProjectHook
end
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index cbfa0c5bc1c..c64ec2fcc95 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -23,7 +23,7 @@ module API
forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :path, type: String, desc: 'The new project path and name'
requires :file, type: File, desc: 'The project export file to be imported'
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index c7137ba5217..da31bcb8dac 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of project milestones' do
success Entities::Milestone
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index f3a1b73b153..a607df411a6 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 0a914f9012e..f5d21d8923f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -128,7 +128,7 @@ module API
end
end
- resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :users, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a user projects' do
success Entities::BasicProjectDetails
end
@@ -224,7 +224,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index 47752f40e58..5af43448727 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -4,14 +4,14 @@ module API
class ProtectedBranches < Grape::API
include PaginationParams
- BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
+ BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a project's protected branches" do
success Entities::ProtectedBranch
end
diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb
index ed1c5f0cc05..ee13473c848 100644
--- a/lib/api/protected_tags.rb
+++ b/lib/api/protected_tags.rb
@@ -4,14 +4,14 @@ module API
class ProtectedTags < Grape::API
include PaginationParams
- TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
+ TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a project's protected tags" do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::ProtectedTag
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index dc844c0bd27..32e05d84491 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index b6fbe8c0235..0c328f7268e 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do
success Entities::ResourceLabelEvent
detail 'This feature was introduced in 11.3'
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 2f15f3a7d76..c60d25b88cb 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -19,7 +19,6 @@ module API
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
- # rubocop: disable CodeReuse/ActiveRecord
post '/' do
attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
@@ -28,10 +27,10 @@ module API
if runner_registration_token_valid?
# Create shared runner. Requires admin access
attributes.merge(runner_type: :instance_type)
- elsif project = Project.find_by(runners_token: params[:token])
+ elsif project = Project.find_by_runners_token(params[:token])
# Create a specific runner for the project
attributes.merge(runner_type: :project_type, projects: [project])
- elsif group = Group.find_by(runners_token: params[:token])
+ elsif group = Group.find_by_runners_token(params[:token])
# Create a specific runner for the group
attributes.merge(runner_type: :group_type, groups: [group])
else
@@ -46,7 +45,6 @@ module API
render_validation_error!(runner)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
desc 'Deletes a registered Runner' do
http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ce70460af11..f72b33605a7 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -126,7 +126,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authorize_admin_project }
desc 'Get runners available for project' do
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 12d97dcfe7f..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?
@@ -70,7 +65,7 @@ module API
end
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Search on GitLab' do
detail 'This feature was introduced in GitLab 10.5.'
end
@@ -89,7 +84,7 @@ module API
end
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Search on GitLab' do
detail 'This feature was introduced in GitLab 10.5.'
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1cb3b8a7277..d60f0f5f08d 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -763,7 +763,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authenticate! }
before { authorize_admin_project }
@@ -842,7 +842,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 077e9373ac4..74ad3c35a61 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -14,7 +14,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index f739eacf9ba..b18eec7d796 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -4,14 +4,14 @@ module API
class Tags < Grape::API
include PaginationParams
- TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
+ TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository tags' do
success Entities::Tag
end
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/api/todos.rb b/lib/api/todos.rb
index ed2cf2cc31b..d2c8cf7c1aa 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_iid".to_sym
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index f784c857883..3ce1529f259 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Trigger a GitLab project pipeline' do
success Entities::Pipeline
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index c844ba321ed..f7cae2251c2 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -11,7 +11,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project variables' do
success Entities::Variable
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 24746f4efc6..302b2797a34 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -22,7 +22,7 @@ module API
end
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of wiki pages' do
success Entities::WikiPageBasic
end
@@ -103,7 +103,7 @@ module API
requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
- post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
authorize! :create_wiki, user_project
result = ::Wikis::CreateAttachmentService.new(user_project,
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index a27f1d46863..c6a3a763c23 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -17,6 +17,9 @@ module Banzai
# This is a small extension to the CommonMark spec. If they start allowing
# spaces in urls, we could then remove this filter.
#
+ # Note: Filter::SanitizationFilter should always be run sometime after this filter
+ # to prevent XSS attacks
+ #
class SpacedLinkFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index be75e34a673..96bea7ca935 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -12,13 +12,16 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::PlantumlFilter,
+
+ # Must always be before the SanitizationFilter to prevent XSS attacks
+ Filter::SpacedLinkFilter,
+
Filter::SanitizationFilter,
Filter::SyntaxHighlightFilter,
Filter::MathFilter,
Filter::ColorFilter,
Filter::MermaidFilter,
- Filter::SpacedLinkFilter,
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 655278da711..b2c8d46ede1 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -110,11 +110,6 @@ module ExtractsPath
# resolved (e.g., when a user inserts an invalid path or ref).
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def assign_ref_vars
- # assign allowed options
- allowed_options = ["filter_ref"]
- @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
- @options = HashWithIndifferentAccess.new(@options)
-
@id = get_id
@ref, @path = extract_ref(@id)
@repo = @project.repository
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index cb9f2582936..176766d1a8b 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -13,12 +13,18 @@ module Gitlab
@request = request
end
- def user
- find_sessionless_user || find_user_from_warden
+ def user(request_formats)
+ request_formats.each do |format|
+ user = find_sessionless_user(format)
+
+ return user if user
+ end
+
+ find_user_from_warden
end
- def find_sessionless_user
- find_user_from_access_token || find_user_from_feed_token
+ def find_sessionless_user(request_format)
+ find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
rescue Gitlab::Auth::AuthenticationError
nil
end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index c304adc64db..a5efe33bdc6 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -7,6 +7,7 @@ module Gitlab
TokenNotFoundError = Class.new(AuthenticationError)
ExpiredError = Class.new(AuthenticationError)
RevokedError = Class.new(AuthenticationError)
+ ImpersonationDisabled = Class.new(AuthenticationError)
UnauthorizedError = Class.new(AuthenticationError)
class InsufficientScopeError < AuthenticationError
@@ -27,8 +28,8 @@ module Gitlab
current_request.env['warden']&.authenticate if verified_request?
end
- def find_user_from_feed_token
- return unless rss_request? || ics_request?
+ def find_user_from_feed_token(request_format)
+ return unless valid_rss_format?(request_format)
# NOTE: feed_token was renamed from rss_token but both needs to be supported because
# users might have already added the feed to their RSS reader before the rename
@@ -38,6 +39,17 @@ module Gitlab
User.find_by_feed_token(token) || raise(UnauthorizedError)
end
+ # We only allow Private Access Tokens with `api` scope to be used by web
+ # requests on RSS feeds or ICS files for backwards compatibility.
+ # It is also used by GraphQL/API requests.
+ def find_user_from_web_access_token(request_format)
+ return unless access_token && valid_web_access_format?(request_format)
+
+ validate_access_token!(scopes: [:api])
+
+ access_token.user || raise(UnauthorizedError)
+ end
+
def find_user_from_access_token
return unless access_token
@@ -56,6 +68,8 @@ module Gitlab
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
+ when AccessTokenValidationService::IMPERSONATION_DISABLED
+ raise ImpersonationDisabled
end
end
@@ -109,6 +123,26 @@ module Gitlab
@current_request ||= ensure_action_dispatch_request(request)
end
+ def valid_web_access_format?(request_format)
+ case request_format
+ when :rss
+ rss_request?
+ when :ics
+ ics_request?
+ when :api
+ api_request?
+ end
+ end
+
+ def valid_rss_format?(request_format)
+ case request_format
+ when :rss
+ rss_request?
+ when :ics
+ ics_request?
+ end
+ end
+
def rss_request?
current_request.path.ends_with?('.atom') || current_request.format.atom?
end
@@ -116,6 +150,10 @@ module Gitlab
def ics_request?
current_request.path.ends_with?('.ics') || current_request.format.ics?
end
+
+ def api_request?
+ current_request.path.starts_with?("/api/")
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
new file mode 100644
index 00000000000..29fa0f18448
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config.rb
@@ -0,0 +1,209 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This module is used to write the full path of all projects to
+ # the git repository config file.
+ # Storing the full project path in the git config allows admins to
+ # easily identify a project when it is using hashed storage.
+ module BackfillProjectFullpathInRepoConfig
+ OrphanedNamespaceError = Class.new(StandardError)
+
+ module Storage
+ # Class that returns the disk path for a project using hashed storage
+ class HashedProject
+ attr_accessor :project
+
+ ROOT_PATH_PREFIX = '@hashed'
+
+ def initialize(project)
+ @project = project
+ end
+
+ def disk_path
+ "#{ROOT_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}/#{disk_hash}"
+ end
+
+ def disk_hash
+ @disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id
+ end
+ end
+
+ # Class that returns the disk path for a project using legacy storage
+ class LegacyProject
+ attr_accessor :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def disk_path
+ project.full_path
+ end
+ end
+ end
+
+ # Concern used by Project and Namespace to determine the full
+ # route to the project
+ module Routable
+ extend ActiveSupport::Concern
+
+ def full_path
+ @full_path ||= build_full_path
+ end
+
+ def build_full_path
+ return path unless has_parent?
+
+ raise OrphanedNamespaceError if parent.nil?
+
+ parent.full_path + '/' + path
+ end
+
+ def has_parent?
+ read_attribute(association(:parent).reflection.foreign_key)
+ end
+ end
+
+ # Class used to interact with repository using Gitaly
+ class Repository
+ attr_reader :storage
+
+ def initialize(storage, relative_path)
+ @storage = storage
+ @relative_path = relative_path
+ end
+
+ def gitaly_repository
+ Gitaly::Repository.new(storage_name: @storage, relative_path: @relative_path)
+ end
+ end
+
+ # Namespace can be a user or group. It can be the root or a
+ # child of another namespace.
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ self.inheritance_column = nil
+
+ include Routable
+
+ belongs_to :parent, class_name: 'Namespace', inverse_of: 'namespaces'
+ has_many :projects, inverse_of: :parent
+ has_many :namespaces, inverse_of: :parent
+ end
+
+ # Project is where the repository (etc.) is stored
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ include Routable
+ include EachBatch
+
+ FULLPATH_CONFIG_KEY = 'gitlab.fullpath'
+
+ belongs_to :parent, class_name: 'Namespace', foreign_key: :namespace_id, inverse_of: 'projects'
+ delegate :disk_path, to: :storage
+
+ def add_fullpath_config
+ entries = { FULLPATH_CONFIG_KEY => full_path }
+
+ repository_service.set_config(entries)
+ end
+
+ def remove_fullpath_config
+ repository_service.delete_config([FULLPATH_CONFIG_KEY])
+ end
+
+ def cleanup_repository
+ repository_service.cleanup
+ end
+
+ def storage
+ @storage ||=
+ if hashed_storage?
+ Storage::HashedProject.new(self)
+ else
+ Storage::LegacyProject.new(self)
+ end
+ end
+
+ def hashed_storage?
+ self.storage_version && self.storage_version >= 1
+ end
+
+ def repository
+ @repository ||= Repository.new(repository_storage, disk_path + '.git')
+ end
+
+ def repository_service
+ @repository_service ||= Gitlab::GitalyClient::RepositoryService.new(repository)
+ end
+ end
+
+ # Base class for Up and Down migration classes
+ class BackfillFullpathMigration
+ RETRY_DELAY = 15.minutes
+ MAX_RETRIES = 2
+
+ # Base class for retrying one project
+ class BaseRetryOne
+ def perform(project_id, retry_count)
+ project = Project.find(project_id)
+
+ return unless project
+
+ migration_class.new.safe_perform_one(project, retry_count)
+ end
+ end
+
+ def perform(start_id, end_id)
+ Project.includes(:parent).where(id: start_id..end_id).each do |project|
+ safe_perform_one(project)
+ end
+ end
+
+ def safe_perform_one(project, retry_count = 0)
+ perform_one(project)
+ rescue GRPC::NotFound, GRPC::InvalidArgument, OrphanedNamespaceError
+ nil
+ rescue GRPC::BadStatus
+ schedule_retry(project, retry_count + 1) if retry_count < MAX_RETRIES
+ end
+
+ def schedule_retry(project, retry_count)
+ BackgroundMigrationWorker.perform_in(RETRY_DELAY, self.class::RetryOne.name, [project.id, retry_count])
+ end
+ end
+
+ # Class to add the fullpath to the git repo config
+ class Up < BackfillFullpathMigration
+ # Class used to retry
+ class RetryOne < BaseRetryOne
+ def migration_class
+ Up
+ end
+ end
+
+ def perform_one(project)
+ project.cleanup_repository
+ project.add_fullpath_config
+ end
+ end
+
+ # Class to rollback adding the fullpath to the git repo config
+ class Down < BackfillFullpathMigration
+ # Class used to retry
+ class RetryOne < BaseRetryOne
+ def migration_class
+ Down
+ end
+ end
+
+ def perform_one(project)
+ project.cleanup_repository
+ project.remove_fullpath_config
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/encrypt_columns.rb b/lib/gitlab/background_migration/encrypt_columns.rb
index bd5f12276ab..b9ad8267e37 100644
--- a/lib/gitlab/background_migration/encrypt_columns.rb
+++ b/lib/gitlab/background_migration/encrypt_columns.rb
@@ -5,15 +5,17 @@ module Gitlab
# EncryptColumn migrates data from an unencrypted column - `foo`, say - to
# an encrypted column - `encrypted_foo`, say.
#
+ # To avoid depending on a particular version of the model in app/, add a
+ # model to `lib/gitlab/background_migration/models/encrypt_columns` and use
+ # it in the migration that enqueues the jobs, so code can be shared.
+ #
# For this background migration to work, the table that is migrated _has_ to
# have an `id` column as the primary key. Additionally, the encrypted column
# should be managed by attr_encrypted, and map to an attribute with the same
# name as the unencrypted column (i.e., the unencrypted column should be
- # shadowed).
+ # shadowed), unless you want to define specific methods / accessors in the
+ # temporary model in `/models/encrypt_columns/your_model.rb`.
#
- # To avoid depending on a particular version of the model in app/, add a
- # model to `lib/gitlab/background_migration/models/encrypt_columns` and use
- # it in the migration that enqueues the jobs, so code can be shared.
class EncryptColumns
def perform(model, attributes, from, to)
model = model.constantize if model.is_a?(String)
@@ -36,6 +38,10 @@ module Gitlab
end
end
+ def clear_migrated_values?
+ true
+ end
+
private
# Build a hash of { attribute => encrypted column name }
@@ -72,7 +78,10 @@ module Gitlab
if instance.changed?
instance.save!
- instance.update_columns(to_clear)
+
+ if clear_migrated_values?
+ instance.update_columns(to_clear)
+ end
end
end
diff --git a/lib/gitlab/background_migration/encrypt_runners_tokens.rb b/lib/gitlab/background_migration/encrypt_runners_tokens.rb
new file mode 100644
index 00000000000..91e559a8765
--- /dev/null
+++ b/lib/gitlab/background_migration/encrypt_runners_tokens.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # EncryptColumn migrates data from an unencrypted column - `foo`, say - to
+ # an encrypted column - `encrypted_foo`, say.
+ #
+ # We only create a subclass here because we want to isolate this migration
+ # (migrating unencrypted runner registration tokens to encrypted columns)
+ # from other `EncryptColumns` migration. This class name is going to be
+ # serialized and stored in Redis and later picked by Sidekiq, so we need to
+ # create a separate class name in order to isolate these migration tasks.
+ #
+ # We can solve this differently, see tech debt issue:
+ #
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/54328
+ #
+ class EncryptRunnersTokens < EncryptColumns
+ def perform(model, from, to)
+ resource = "::Gitlab::BackgroundMigration::Models::EncryptColumns::#{model.to_s.capitalize}"
+ model = resource.constantize
+ attributes = model.encrypted_attributes.keys
+
+ super(model, attributes, from, to)
+ end
+
+ def clear_migrated_values?
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/models/encrypt_columns/namespace.rb b/lib/gitlab/background_migration/models/encrypt_columns/namespace.rb
new file mode 100644
index 00000000000..41f18979d76
--- /dev/null
+++ b/lib/gitlab/background_migration/models/encrypt_columns/namespace.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module Models
+ module EncryptColumns
+ # This model is shared between synchronous and background migrations to
+ # encrypt the `runners_token` column in `namespaces` table.
+ #
+ class Namespace < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
+
+ def runners_token=(value)
+ self.runners_token_encrypted =
+ ::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
+ end
+
+ def self.encrypted_attributes
+ { runners_token: { attribute: :runners_token_encrypted } }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/models/encrypt_columns/project.rb b/lib/gitlab/background_migration/models/encrypt_columns/project.rb
new file mode 100644
index 00000000000..bfeae14584d
--- /dev/null
+++ b/lib/gitlab/background_migration/models/encrypt_columns/project.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module Models
+ module EncryptColumns
+ # This model is shared between synchronous and background migrations to
+ # encrypt the `runners_token` column in `projects` table.
+ #
+ class Project < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'projects'
+ self.inheritance_column = :_type_disabled
+
+ def runners_token=(value)
+ self.runners_token_encrypted =
+ ::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
+ end
+
+ def self.encrypted_attributes
+ { runners_token: { attribute: :runners_token_encrypted } }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/models/encrypt_columns/runner.rb b/lib/gitlab/background_migration/models/encrypt_columns/runner.rb
new file mode 100644
index 00000000000..14ddce4b147
--- /dev/null
+++ b/lib/gitlab/background_migration/models/encrypt_columns/runner.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module Models
+ module EncryptColumns
+ # This model is shared between synchronous and background migrations to
+ # encrypt the `token` column in `ci_runners` table.
+ #
+ class Runner < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'ci_runners'
+ self.inheritance_column = :_type_disabled
+
+ def token=(value)
+ self.token_encrypted =
+ ::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
+ end
+
+ def self.encrypted_attributes
+ { token: { attribute: :token_encrypted } }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/models/encrypt_columns/settings.rb b/lib/gitlab/background_migration/models/encrypt_columns/settings.rb
new file mode 100644
index 00000000000..08ae35c0671
--- /dev/null
+++ b/lib/gitlab/background_migration/models/encrypt_columns/settings.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ module Models
+ module EncryptColumns
+ # This model is shared between synchronous and background migrations to
+ # encrypt the `runners_token` column in `application_settings` table.
+ #
+ class Settings < ActiveRecord::Base
+ include ::EachBatch
+ include ::CacheableAttributes
+
+ self.table_name = 'application_settings'
+ self.inheritance_column = :_type_disabled
+
+ after_commit do
+ ::ApplicationSetting.expire
+ end
+
+ def runners_registration_token=(value)
+ self.runners_registration_token_encrypted =
+ ::Gitlab::CryptoHelper.aes256_gcm_encrypt(value)
+ end
+
+ def self.encrypted_attributes
+ {
+ runners_registration_token: {
+ attribute: :runners_registration_token_encrypted
+ }
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb b/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb
index bb76eb8ed48..34e72fd9f34 100644
--- a/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb
+++ b/lib/gitlab/background_migration/models/encrypt_columns/web_hook.rb
@@ -15,12 +15,12 @@ module Gitlab
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_truncated
+ key: ::Settings.attr_encrypted_db_key_base_32
attr_encrypted :url,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_truncated
+ key: ::Settings.attr_encrypted_db_key_base_32
end
end
end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index a7fcb6b0fca..7f7cc62c8ef 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -14,7 +14,7 @@ module Gitlab
@ref = ref
@job = job
- @pipeline = @project.pipelines.latest_successful_for(@ref)
+ @pipeline = @project.ci_pipelines.latest_successful_for(@ref)
end
def entity
diff --git a/lib/gitlab/badge/pipeline/status.rb b/lib/gitlab/badge/pipeline/status.rb
index 37e61f07e5b..a403d839517 100644
--- a/lib/gitlab/badge/pipeline/status.rb
+++ b/lib/gitlab/badge/pipeline/status.rb
@@ -22,7 +22,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def status
- @project.pipelines
+ @project.ci_pipelines
.where(sha: @sha)
.latest_status(@ref) || 'unknown'
end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 45e550b3450..eaead41a720 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -35,7 +35,7 @@ module Gitlab
def handle_errors
return unless errors.any?
- project.update_column(:import_error, {
+ project.import_state.update_column(:last_error, {
message: 'The remote data could not be fully imported.',
errors: errors
}.to_json)
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index 15aa4739ee9..d4080536d81 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -56,7 +56,7 @@ module Gitlab
def handle_errors
return unless errors.any?
- project.update_column(:import_error, {
+ project.import_state.update_column(:last_error, {
message: 'The remote data could not be fully imported.',
errors: errors
}.to_json)
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/base_checker.rb b/lib/gitlab/checks/base_checker.rb
new file mode 100644
index 00000000000..f8cda0382fe
--- /dev/null
+++ b/lib/gitlab/checks/base_checker.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class BaseChecker
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :change_access
+ delegate(*ChangeAccess::ATTRIBUTES, to: :change_access)
+
+ def initialize(change_access)
+ @change_access = change_access
+ end
+
+ def validate!
+ raise NotImplementedError
+ end
+
+ private
+
+ def deletion?
+ Gitlab::Git.blank_ref?(newrev)
+ end
+
+ def update?
+ !Gitlab::Git.blank_ref?(oldrev) && !deletion?
+ end
+
+ def updated_from_web?
+ protocol == 'web'
+ end
+
+ def tag_exists?
+ project.repository.tag_exists?(tag_name)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
new file mode 100644
index 00000000000..d06b2df36f2
--- /dev/null
+++ b/lib/gitlab/checks/branch_check.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class BranchCheck < BaseChecker
+ ERROR_MESSAGES = {
+ delete_default_branch: 'The default branch of a project cannot be deleted.',
+ force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
+ non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.',
+ non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
+ merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
+ push_protected_branch: 'You are not allowed to push code to protected branches on this project.'
+ }.freeze
+
+ LOG_MESSAGES = {
+ delete_default_branch_check: "Checking if default branch is being deleted...",
+ protected_branch_checks: "Checking if you are force pushing to a protected branch...",
+ protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
+ protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch..."
+ }.freeze
+
+ def validate!
+ return unless branch_name
+
+ logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
+ if deletion? && branch_name == project.default_branch
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ end
+ end
+
+ protected_branch_checks
+ end
+
+ private
+
+ def protected_branch_checks
+ logger.log_timed(LOG_MESSAGES[:protected_branch_checks]) do
+ return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
+
+ if forced_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ end
+ end
+
+ if deletion?
+ protected_branch_deletion_checks
+ else
+ protected_branch_push_checks
+ end
+ end
+
+ def protected_branch_deletion_checks
+ logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do
+ unless user_access.can_delete_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
+ end
+
+ unless updated_from_web?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ end
+ end
+ end
+
+ def protected_branch_push_checks
+ logger.log_timed(LOG_MESSAGES[:protected_branch_push_checks]) do
+ if matching_merge_request?
+ unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
+ end
+ else
+ unless user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ end
+ end
+ end
+ end
+
+ def push_to_protected_branch_rejected_message
+ if project.empty_repo?
+ empty_project_push_message
+ else
+ ERROR_MESSAGES[:push_protected_branch]
+ end
+ end
+
+ def empty_project_push_message
+ <<~MESSAGE
+
+ A default branch (e.g. master) does not yet exist for #{project.full_path}
+ Ask a project Owner or Maintainer to create a default branch:
+
+ #{project_members_url}
+
+ MESSAGE
+ end
+
+ def project_members_url
+ Gitlab::Routing.url_helpers.project_project_members_url(project)
+ end
+
+ def matching_merge_request?
+ Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
+ end
+
+ def forced_push?
+ Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 074afe9c412..7778d3068cc 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -3,35 +3,11 @@
module Gitlab
module Checks
class ChangeAccess
- ERROR_MESSAGES = {
- push_code: 'You are not allowed to push code to this project.',
- delete_default_branch: 'The default branch of a project cannot be deleted.',
- force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
- non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.',
- non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
- merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
- push_protected_branch: 'You are not allowed to push code to protected branches on this project.',
- change_existing_tags: 'You are not allowed to change existing tags on this project.',
- update_protected_tag: 'Protected tags cannot be updated.',
- delete_protected_tag: 'Protected tags cannot be deleted.',
- create_protected_tag: 'You are not allowed to create this tag as it is protected.',
- lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
- }.freeze
+ ATTRIBUTES = %i[user_access project skip_authorization
+ skip_lfs_integrity_check protocol oldrev newrev ref
+ branch_name tag_name logger commits].freeze
- LOG_MESSAGES = {
- push_checks: "Checking if you are allowed to push...",
- delete_default_branch_check: "Checking if default branch is being deleted...",
- protected_branch_checks: "Checking if you are force pushing to a protected branch...",
- protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
- protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...",
- tag_checks: "Checking if you are allowed to change existing tags...",
- protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...",
- lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...",
- commits_check_file_paths_validation: "Validating commits' file paths...",
- commits_check: "Validating commit contents..."
- }.freeze
-
- attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger
+ attr_reader(*ATTRIBUTES)
def initialize(
change, user_access:, project:, skip_authorization: false,
@@ -50,206 +26,32 @@ module Gitlab
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
- def exec(skip_commits_check: false)
+ def exec
return true if skip_authorization
- push_checks
- branch_checks
- tag_checks
- lfs_objects_exist_check unless skip_lfs_integrity_check
- commits_check unless skip_commits_check
+ ref_level_checks
+ # Check of commits should happen as the last step
+ # given they're expensive in terms of performance
+ commits_check
true
end
- protected
-
- def push_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- unless can_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
- end
- end
- end
-
- def branch_checks
- return unless branch_name
-
- logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
- if deletion? && branch_name == project.default_branch
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
- end
- end
-
- protected_branch_checks
- end
-
- def protected_branch_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
-
- if forced_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
- end
- end
-
- if deletion?
- protected_branch_deletion_checks
- else
- protected_branch_push_checks
- end
- end
-
- def protected_branch_deletion_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- unless user_access.can_delete_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
- end
-
- unless updated_from_web?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
- end
- end
- end
-
- def protected_branch_push_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- if matching_merge_request?
- unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
- end
- else
- unless user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
- end
- end
- end
- end
-
- def tag_checks
- return unless tag_name
-
- logger.log_timed(LOG_MESSAGES[__method__]) do
- if tag_exists? && user_access.cannot_do_action?(:admin_project)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
- end
- end
-
- protected_tag_checks
+ def commits
+ @commits ||= project.repository.new_commits(newrev)
end
- def protected_tag_checks
- logger.log_timed(LOG_MESSAGES[__method__]) do
- return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
-
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+ protected
- unless user_access.can_create_tag?(tag_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
- end
- end
+ def ref_level_checks
+ Gitlab::Checks::PushCheck.new(self).validate!
+ Gitlab::Checks::BranchCheck.new(self).validate!
+ Gitlab::Checks::TagCheck.new(self).validate!
+ Gitlab::Checks::LfsCheck.new(self).validate!
end
def commits_check
- return if deletion? || newrev.nil?
- return unless should_run_commit_validations?
-
- logger.log_timed(LOG_MESSAGES[__method__]) do
- # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- commits.each do |commit|
- logger.check_timeout_reached
-
- commit_check.validate(commit, validations_for_commit(commit))
- end
- end
- end
-
- logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do
- commit_check.validate_file_paths
- end
- end
-
- # Method overwritten in EE to inject custom validations
- def validations_for_commit(_)
- []
- end
-
- private
-
- def push_to_protected_branch_rejected_message
- if project.empty_repo?
- empty_project_push_message
- else
- ERROR_MESSAGES[:push_protected_branch]
- end
- end
-
- def empty_project_push_message
- <<~MESSAGE
-
- A default branch (e.g. master) does not yet exist for #{project.full_path}
- Ask a project Owner or Maintainer to create a default branch:
-
- #{project_members_url}
-
- MESSAGE
- end
-
- def project_members_url
- Gitlab::Routing.url_helpers.project_project_members_url(project)
- end
-
- def should_run_commit_validations?
- commit_check.validate_lfs_file_locks?
- end
-
- def updated_from_web?
- protocol == 'web'
- end
-
- def tag_exists?
- project.repository.tag_exists?(tag_name)
- end
-
- def forced_push?
- Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
- end
-
- def update?
- !Gitlab::Git.blank_ref?(oldrev) && !deletion?
- end
-
- def deletion?
- Gitlab::Git.blank_ref?(newrev)
- end
-
- def matching_merge_request?
- Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
- end
-
- def lfs_objects_exist_check
- logger.log_timed(LOG_MESSAGES[__method__]) do
- lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
-
- if lfs_check.objects_missing?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
- end
- end
- end
-
- def commit_check
- @commit_check ||= Gitlab::Checks::CommitCheck.new(project, user_access.user, newrev, oldrev)
- end
-
- def commits
- @commits ||= project.repository.new_commits(newrev)
- end
-
- def can_push?
- user_access.can_do_action?(:push_code) ||
- user_access.can_push_to_branch?(branch_name)
+ Gitlab::Checks::DiffCheck.new(self).validate!
end
end
end
diff --git a/lib/gitlab/checks/commit_check.rb b/lib/gitlab/checks/commit_check.rb
deleted file mode 100644
index 58267b6752f..00000000000
--- a/lib/gitlab/checks/commit_check.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Checks
- class CommitCheck
- include Gitlab::Utils::StrongMemoize
-
- attr_reader :project, :user, :newrev, :oldrev
-
- def initialize(project, user, newrev, oldrev)
- @project = project
- @user = user
- @newrev = newrev
- @oldrev = oldrev
- @file_paths = []
- end
-
- def validate(commit, validations)
- return if validations.empty? && path_validations.empty?
-
- commit.raw_deltas.each do |diff|
- @file_paths << (diff.new_path || diff.old_path)
-
- validations.each do |validation|
- if error = validation.call(diff)
- raise ::Gitlab::GitAccess::UnauthorizedError, error
- end
- end
- end
- end
-
- def validate_file_paths
- path_validations.each do |validation|
- if error = validation.call(@file_paths)
- raise ::Gitlab::GitAccess::UnauthorizedError, error
- end
- end
- end
-
- def validate_lfs_file_locks?
- strong_memoize(:validate_lfs_file_locks) do
- project.lfs_enabled? && newrev && oldrev && project.any_lfs_file_locks?
- end
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def lfs_file_locks_validation
- lambda do |paths|
- lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user.id).first
-
- if lfs_lock
- return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}"
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def path_validations
- validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
- end
- end
- end
-end
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
new file mode 100644
index 00000000000..49d361fcef7
--- /dev/null
+++ b/lib/gitlab/checks/diff_check.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class DiffCheck < BaseChecker
+ include Gitlab::Utils::StrongMemoize
+
+ LOG_MESSAGES = {
+ validate_file_paths: "Validating diffs' file paths...",
+ diff_content_check: "Validating diff contents..."
+ }.freeze
+
+ def validate!
+ return unless should_run_diff_validations?
+ return if commits.empty?
+ return unless uses_raw_delta_validations?
+
+ file_paths = []
+ process_raw_deltas do |diff|
+ file_paths << (diff.new_path || diff.old_path)
+
+ validate_diff(diff)
+ end
+
+ validate_file_paths(file_paths)
+ end
+
+ private
+
+ def should_run_diff_validations?
+ newrev && oldrev && !deletion? && validate_lfs_file_locks?
+ end
+
+ def validate_lfs_file_locks?
+ strong_memoize(:validate_lfs_file_locks) do
+ project.lfs_enabled? && project.any_lfs_file_locks?
+ end
+ end
+
+ def uses_raw_delta_validations?
+ validations_for_diff.present? || path_validations.present?
+ end
+
+ def validate_diff(diff)
+ validations_for_diff.each do |validation|
+ if error = validation.call(diff)
+ raise ::Gitlab::GitAccess::UnauthorizedError, error
+ end
+ end
+ end
+
+ # Method overwritten in EE to inject custom validations
+ def validations_for_diff
+ []
+ end
+
+ def path_validations
+ validate_lfs_file_locks? ? [lfs_file_locks_validation] : []
+ end
+
+ def process_raw_deltas
+ logger.log_timed(LOG_MESSAGES[:diff_content_check]) do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits.each do |commit|
+ logger.check_timeout_reached
+
+ commit.raw_deltas.each do |diff|
+ yield(diff)
+ end
+ end
+ end
+ end
+ end
+
+ def validate_file_paths(file_paths)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ path_validations.each do |validation|
+ if error = validation.call(file_paths)
+ raise ::Gitlab::GitAccess::UnauthorizedError, error
+ end
+ end
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def lfs_file_locks_validation
+ lambda do |paths|
+ lfs_lock = project.lfs_file_locks.where(path: paths).where.not(user_id: user_access.user.id).take
+
+ if lfs_lock
+ return "The path '#{lfs_lock.path}' is locked in Git LFS by #{lfs_lock.user.name}"
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/gitlab/checks/lfs_check.rb b/lib/gitlab/checks/lfs_check.rb
new file mode 100644
index 00000000000..e42684e679a
--- /dev/null
+++ b/lib/gitlab/checks/lfs_check.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class LfsCheck < BaseChecker
+ LOG_MESSAGE = "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...".freeze
+ ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'.freeze
+
+ def validate!
+ return if skip_lfs_integrity_check
+
+ logger.log_timed(LOG_MESSAGE) do
+ lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
+
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGE
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb
new file mode 100644
index 00000000000..f3a52f09868
--- /dev/null
+++ b/lib/gitlab/checks/push_check.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class PushCheck < BaseChecker
+ def validate!
+ logger.log_timed("Checking if you are allowed to push...") do
+ unless can_push?
+ raise GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.'
+ end
+ end
+ end
+
+ private
+
+ def can_push?
+ user_access.can_do_action?(:push_code) ||
+ user_access.can_push_to_branch?(branch_name)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb
new file mode 100644
index 00000000000..2a75c8059bd
--- /dev/null
+++ b/lib/gitlab/checks/tag_check.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class TagCheck < BaseChecker
+ ERROR_MESSAGES = {
+ change_existing_tags: 'You are not allowed to change existing tags on this project.',
+ update_protected_tag: 'Protected tags cannot be updated.',
+ delete_protected_tag: 'Protected tags cannot be deleted.',
+ create_protected_tag: 'You are not allowed to create this tag as it is protected.'
+ }.freeze
+
+ LOG_MESSAGES = {
+ tag_checks: "Checking if you are allowed to change existing tags...",
+ protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag..."
+ }.freeze
+
+ def validate!
+ return unless tag_name
+
+ logger.log_timed(LOG_MESSAGES[:tag_checks]) do
+ if tag_exists? && user_access.cannot_do_action?(:admin_project)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ end
+ end
+
+ protected_tag_checks
+ end
+
+ private
+
+ def protected_tag_checks
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
+
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+
+ unless user_access.can_create_tag?(tag_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
index a4f01468e8e..7cabaadb122 100644
--- a/lib/gitlab/ci/charts.rb
+++ b/lib/gitlab/ci/charts.rb
@@ -54,7 +54,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def collect
- query = project.pipelines
+ query = project.all_pipelines
.where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
totals_count = grouped_count(query)
@@ -115,7 +115,7 @@ module Gitlab
class PipelineTime < Chart
def collect
- commits = project.pipelines.last(30)
+ commits = project.all_pipelines.last(30)
commits.each do |commit|
@labels << commit.short_sha
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 2fb3c4582e7..6333799a491 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -15,7 +15,7 @@ module Gitlab
@global = Entry::Global.new(@config)
@global.compose!
- rescue Loader::FormatError,
+ rescue Gitlab::Config::Loader::FormatError,
Extendable::ExtensionError,
External::Processor::IncludeError => e
raise Config::ConfigError, e.message
@@ -71,7 +71,7 @@ module Gitlab
private
def build_config(config, opts = {})
- initial_config = Loader.new(config).load!
+ initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
project = opts.fetch(:project, nil)
if project
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index ef5f25b42c0..41613369ca2 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -7,10 +7,10 @@ module Gitlab
##
# Entry that represents a configuration of job artifacts.
#
- class Artifacts < Node
- include Configurable
- include Validatable
- include Attributable
+ class Artifacts < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
deleted file mode 100644
index 3c2e1df9b83..00000000000
--- a/lib/gitlab/ci/config/entry/attributable.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Attributable
- extend ActiveSupport::Concern
-
- class_methods do
- def attributes(*attributes)
- attributes.flatten.each do |attribute|
- if method_defined?(attribute)
- raise ArgumentError, 'Method already defined!'
- end
-
- define_method(attribute) do
- return unless config.is_a?(Hash)
-
- config[attribute]
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
deleted file mode 100644
index b9639c83075..00000000000
--- a/lib/gitlab/ci/config/entry/boolean.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Entry that represents a boolean value.
- #
- class Boolean < Node
- include Validatable
-
- validations do
- validates :config, boolean: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 0a25057f482..7b94af24c09 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a cache configuration
#
- class Cache < Node
- include Configurable
- include Attributable
+ class Cache < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths policy].freeze
DEFAULT_POLICY = 'pull-push'.freeze
@@ -22,7 +22,7 @@ module Gitlab
entry :key, Entry::Key,
description: 'Cache key used to define a cache affinity.'
- entry :untracked, Entry::Boolean,
+ entry :untracked, ::Gitlab::Config::Entry::Boolean,
description: 'Cache all untracked files.'
entry :paths, Entry::Paths,
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index d9658291ebe..02e368c1813 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a job script.
#
- class Commands < Node
- include Validatable
+ class Commands < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings_or_string: true
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
deleted file mode 100644
index 4aabf0cfa31..00000000000
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This mixin is responsible for adding DSL, which purpose is to
- # simplifly process of adding child nodes.
- #
- # This can be used only if parent node is a configuration entry that
- # holds a hash as a configuration value, for example:
- #
- # job:
- # script: ...
- # artifacts: ...
- #
- module Configurable
- extend ActiveSupport::Concern
-
- included do
- include Validatable
-
- validations do
- validates :config, type: Hash
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def compose!(deps = nil)
- return unless valid?
-
- self.class.nodes.each do |key, factory|
- factory
- .value(config[key])
- .with(key: key, parent: self)
-
- entries[key] = factory.create!
- end
-
- yield if block_given?
-
- entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- class_methods do
- def nodes
- Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def entry(key, entry, metadata)
- factory = Entry::Factory.new(entry)
- .with(description: metadata[:description])
-
- (@nodes ||= {}).merge!(key.to_sym => factory)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def helpers(*nodes)
- nodes.each do |symbol|
- define_method("#{symbol}_defined?") do
- entries[symbol]&.specified?
- end
-
- define_method("#{symbol}_value") do
- return unless entries[symbol] && entries[symbol].valid?
-
- entries[symbol].value
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
index 690409ccf77..89545158bed 100644
--- a/lib/gitlab/ci/config/entry/coverage.rb
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents Coverage settings.
#
- class Coverage < Node
- include Validatable
+ class Coverage < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, regexp: true
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 07e9e1d3f67..69a3a1aedef 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents an environment.
#
- class Environment < Node
- include Validatable
+ class Environment < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name url action on_stop].freeze
diff --git a/lib/gitlab/ci/config/entry/except_policy.rb b/lib/gitlab/ci/config/entry/except_policy.rb
new file mode 100644
index 00000000000..46ded35325d
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/except_policy.rb
@@ -0,0 +1,17 @@
+# 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/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
deleted file mode 100644
index 85c9c3511a4..00000000000
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Factory class responsible for fabricating entry objects.
- #
- class Factory
- InvalidFactory = Class.new(StandardError)
-
- def initialize(entry)
- @entry = entry
- @metadata = {}
- @attributes = {}
- end
-
- def value(value)
- @value = value
- self
- end
-
- def metadata(metadata)
- @metadata.merge!(metadata)
- self
- end
-
- def with(attributes)
- @attributes.merge!(attributes)
- self
- end
-
- def create!
- raise InvalidFactory unless defined?(@value)
-
- ##
- # We assume that unspecified entry is undefined.
- # See issue #18775.
- #
- if @value.nil?
- Entry::Unspecified.new(
- fabricate_unspecified
- )
- else
- fabricate(@entry, @value)
- end
- end
-
- private
-
- def fabricate_unspecified
- ##
- # If entry has a default value we fabricate concrete node
- # with default value.
- #
- if @entry.default.nil?
- fabricate(Entry::Undefined)
- else
- fabricate(@entry, @entry.default)
- end
- end
-
- def fabricate(entry, value = nil)
- entry.new(value, @metadata).tap do |node|
- node.key = @attributes[:key]
- node.parent = @attributes[:parent]
- node.description = @attributes[:description]
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index eba203d9d06..09ecb5fdb99 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -8,8 +8,8 @@ module Gitlab
# This class represents a global entry - root Entry for entire
# GitLab CI Configuration file.
#
- class Global < Node
- include Configurable
+ class Global < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.'
@@ -49,7 +49,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def compose_jobs!
- factory = Entry::Factory.new(Entry::Jobs)
+ factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys))
.with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline')
diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
index dc0ede2a25f..76e5d05639f 100644
--- a/lib/gitlab/ci/config/entry/hidden.rb
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a hidden CI/CD key.
#
- class Hidden < Node
- include Validatable
+ class Hidden < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index fc453b72fa5..a13a0625e90 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a Docker image.
#
- class Image < Node
- include Validatable
+ class Image < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint].freeze
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index c8cb3248fa7..085be5da08d 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a concrete CI/CD job.
#
- class Job < Node
- include Configurable
- include Attributable
+ class Job < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[tags script only except type image services
allow_failure type stage when start_in artifacts cache
@@ -65,10 +65,10 @@ module Gitlab
entry :services, Entry::Services,
description: 'Services that will be used to execute this job.'
- entry :only, Entry::Policy,
+ entry :only, Entry::OnlyPolicy,
description: 'Refs policy this job will be executed for.'
- entry :except, Entry::Policy,
+ entry :except, Entry::ExceptPolicy,
description: 'Refs policy this job will be executed for.'
entry :variables, Entry::Variables,
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 1535b108000..82b72e40404 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a set of jobs.
#
- class Jobs < Node
- include Validatable
+ class Jobs < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: Hash
@@ -34,7 +34,7 @@ module Gitlab
@config.each do |name, config|
node = hidden?(name) ? Entry::Hidden : Entry::Job
- factory = Entry::Factory.new(node)
+ factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 963b200c7bb..0c10967e629 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a key.
#
- class Key < Node
- include Validatable
+ class Key < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, key: true
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
deleted file mode 100644
index 4043629dea9..00000000000
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module LegacyValidationHelpers
- private
-
- def validate_duration(value)
- value.is_a?(String) && ChronicDuration.parse(value)
- rescue ChronicDuration::DurationParseError
- false
- end
-
- def validate_duration_limit(value, limit)
- return false unless value.is_a?(String)
-
- ChronicDuration.parse(value).second.from_now <
- ChronicDuration.parse(limit).second.from_now
- rescue ChronicDuration::DurationParseError
- false
- end
-
- def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? { |value| validate_string(value) }
- end
-
- def validate_array_of_strings_or_regexps(values)
- values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
- end
-
- def validate_variables(variables)
- variables.is_a?(Hash) &&
- variables.flatten.all? do |value|
- validate_string(value) || validate_integer(value)
- end
- end
-
- def validate_integer(value)
- value.is_a?(Integer)
- end
-
- def validate_string(value)
- value.is_a?(String) || value.is_a?(Symbol)
- end
-
- def validate_regexp(value)
- !value.nil? && Regexp.new(value.to_s) && true
- rescue RegexpError, TypeError
- false
- end
-
- def validate_string_or_regexp(value)
- return true if value.is_a?(Symbol)
- return false unless value.is_a?(String)
-
- if value.first == '/' && value.last == '/'
- validate_regexp(value[1...-1])
- else
- true
- end
- end
-
- def validate_boolean(value)
- value.in?([true, false])
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
deleted file mode 100644
index 347089722e4..00000000000
--- a/lib/gitlab/ci/config/entry/node.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Base abstract class for each configuration entry node.
- #
- class Node
- InvalidError = Class.new(StandardError)
-
- attr_reader :config, :metadata
- attr_accessor :key, :parent, :description
-
- def initialize(config, **metadata)
- @config = config
- @metadata = metadata
- @entries = {}
-
- self.class.aspects.to_a.each do |aspect|
- instance_exec(&aspect)
- end
- end
-
- def [](key)
- @entries[key] || Entry::Undefined.new
- end
-
- def compose!(deps = nil)
- return unless valid?
-
- yield if block_given?
- end
-
- def leaf?
- @entries.none?
- end
-
- def descendants
- @entries.values
- end
-
- def ancestors
- @parent ? @parent.ancestors + [@parent] : []
- end
-
- def valid?
- errors.none?
- end
-
- def errors
- []
- end
-
- def value
- if leaf?
- @config
- else
- meaningful = @entries.select do |_key, value|
- value.specified? && value.relevant?
- end
-
- Hash[meaningful.map { |key, entry| [key, entry.value] }]
- end
- end
-
- def specified?
- true
- end
-
- def relevant?
- true
- end
-
- def location
- name = @key.presence || self.class.name.to_s.demodulize
- .underscore.humanize.downcase
-
- ancestors.map(&:key).append(name).compact.join(':')
- end
-
- def inspect
- val = leaf? ? config : descendants
- unspecified = specified? ? '' : '(unspecified) '
- "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
- end
-
- def self.default
- end
-
- def self.aspects
- @aspects ||= []
- end
-
- private
-
- attr_reader :entries
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/only_policy.rb b/lib/gitlab/ci/config/entry/only_policy.rb
new file mode 100644
index 00000000000..9a581b8e97e
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/only_policy.rb
@@ -0,0 +1,18 @@
+# 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/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
index 9580b5e2e7f..d6f287c6552 100644
--- a/lib/gitlab/ci/config/entry/paths.rb
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents an array of paths.
#
- class Paths < Node
- include Validatable
+ class Paths < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 0535d7c1a1a..81e74a639fc 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -5,14 +5,11 @@ module Gitlab
class Config
module Entry
##
- # Entry that represents an only/except trigger policy for the job.
+ # Base class for OnlyPolicy and ExceptPolicy
#
- class Policy < Simplifiable
- strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
- strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
-
- class RefsPolicy < Entry::Node
- include Entry::Validatable
+ class Policy < ::Gitlab::Config::Entry::Simplifiable
+ class RefsPolicy < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings_or_regexps: true
@@ -23,9 +20,9 @@ module Gitlab
end
end
- class ComplexPolicy < Entry::Node
- include Entry::Validatable
- include Entry::Attributable
+ class ComplexPolicy < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze
attributes :refs, :kubernetes, :variables, :changes
@@ -58,7 +55,7 @@ module Gitlab
end
end
- class UnknownStrategy < Entry::Node
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} has to be either an array of conditions or a hash"]
end
@@ -66,6 +63,16 @@ 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/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 3ac2a6fa777..a3f6cc31321 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a configuration of job artifacts.
#
- class Reports < Node
- include Validatable
- include Attributable
+ class Reports < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
index ee82ab10f9c..eaf8b38aa3c 100644
--- a/lib/gitlab/ci/config/entry/retry.rb
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -7,12 +7,12 @@ module Gitlab
##
# Entry that represents a retry config for a job.
#
- class Retry < Simplifiable
+ class Retry < ::Gitlab::Config::Entry::Simplifiable
strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
- class SimpleRetry < Entry::Node
- include Entry::Validatable
+ class SimpleRetry < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, numericality: { only_integer: true,
@@ -31,9 +31,9 @@ module Gitlab
end
end
- class FullRetry < Entry::Node
- include Entry::Validatable
- include Entry::Attributable
+ class FullRetry < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[max when].freeze
attributes :max, :when
@@ -73,7 +73,7 @@ module Gitlab
end
end
- class UnknownStrategy < Entry::Node
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} has to be either an integer or a hash"]
end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index f7d39e5cf55..9d25a82b521 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a script.
#
- class Script < Node
- include Validatable
+ class Script < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 47bf9205147..6df67083310 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -8,7 +8,7 @@ module Gitlab
# Entry that represents a configuration of Docker service.
#
class Service < Image
- include Validatable
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint command alias].freeze
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index bdf7f80f382..71475f69218 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a configuration of Docker services.
#
- class Services < Node
- include Validatable
+ class Services < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: Array
@@ -18,7 +18,7 @@ module Gitlab
super do
@entries = []
@config.each do |config|
- @entries << Entry::Factory.new(Entry::Service)
+ @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {})
.create!
end
diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb
deleted file mode 100644
index 9961bbfaa40..00000000000
--- a/lib/gitlab/ci/config/entry/simplifiable.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- class Simplifiable < SimpleDelegator
- EntryStrategy = Struct.new(:name, :condition)
-
- def initialize(config, **metadata)
- unless self.class.const_defined?(:UnknownStrategy)
- raise ArgumentError, 'UndefinedStrategy not available!'
- end
-
- strategy = self.class.strategies.find do |variant|
- variant.condition.call(config)
- end
-
- entry = self.class.entry_class(strategy)
-
- super(entry.new(config, metadata))
- end
-
- def self.strategy(name, **opts)
- EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
- strategies.append(strategy)
- end
- end
-
- def self.strategies
- @strategies ||= []
- end
-
- def self.entry_class(strategy)
- if strategy.present?
- self.const_get(strategy.name)
- else
- self::UnknownStrategy
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
index 65ab5953131..d6d576a3139 100644
--- a/lib/gitlab/ci/config/entry/stage.rb
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a stage for a job.
#
- class Stage < Node
- include Validatable
+ class Stage < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: String
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index ab184246d29..2d715cbc6bb 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a configuration for pipeline stages.
#
- class Stages < Node
- include Validatable
+ class Stages < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
deleted file mode 100644
index 77dcfa88170..00000000000
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This class represents an undefined entry.
- #
- class Undefined < Node
- def initialize(*)
- super(nil)
- end
-
- def value
- nil
- end
-
- def valid?
- true
- end
-
- def errors
- []
- end
-
- def specified?
- false
- end
-
- def relevant?
- false
- end
-
- def inspect
- "#<#{self.class.name}>"
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
deleted file mode 100644
index bab32489d2f..00000000000
--- a/lib/gitlab/ci/config/entry/unspecified.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This class represents an unspecified entry.
- #
- # It decorates original entry adding method that indicates it is
- # unspecified.
- #
- class Unspecified < SimpleDelegator
- def specified?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
deleted file mode 100644
index 08a6593c980..00000000000
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Validatable
- extend ActiveSupport::Concern
-
- def self.included(node)
- node.aspects.append -> do
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
- end
- end
-
- def errors
- @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
- class_methods do
- def validator
- @validator ||= Class.new(Entry::Validator).tap do |validator|
- if defined?(@validations)
- @validations.each { |rules| validator.class_eval(&rules) }
- end
- end
- end
-
- private
-
- def validations(&block)
- (@validations ||= []).append(block)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
deleted file mode 100644
index 33ffdd3a95d..00000000000
--- a/lib/gitlab/ci/config/entry/validator.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- class Validator < SimpleDelegator
- include ActiveModel::Validations
- include Entry::Validators
-
- def initialize(entry)
- super(entry)
- end
-
- def messages
- errors.full_messages.map do |error|
- "#{location} #{error}".downcase
- end
- end
-
- def self.name
- 'Validator'
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
deleted file mode 100644
index a1d552fb2e5..00000000000
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ /dev/null
@@ -1,198 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Validators
- class AllowedKeysValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unknown_keys = value.try(:keys).to_a - options[:in]
-
- if unknown_keys.any?
- record.errors.add(attribute, "contains unknown keys: " +
- unknown_keys.join(', '))
- end
- end
- end
-
- class AllowedValuesValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless options[:in].include?(value.to_s)
- record.errors.add(attribute, "unknown value: #{value}")
- end
- end
- end
-
- class AllowedArrayValuesValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unkown_values = value - options[:in]
- unless unkown_values.empty?
- record.errors.add(attribute, "contains unknown values: " +
- unkown_values.join(', '))
- end
- end
- end
-
- class ArrayOfStringsValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_array_of_strings(value)
- record.errors.add(attribute, 'should be an array of strings')
- end
- end
- end
-
- class BooleanValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_boolean(value)
- record.errors.add(attribute, 'should be a boolean value')
- end
- end
- end
-
- class DurationValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_duration(value)
- record.errors.add(attribute, 'should be a duration')
- end
-
- if options[:limit]
- unless validate_duration_limit(value, options[:limit])
- record.errors.add(attribute, 'should not exceed the limit')
- end
- end
- end
- end
-
- class HashOrStringValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless value.is_a?(Hash) || value.is_a?(String)
- record.errors.add(attribute, 'should be a hash or a string')
- end
- end
- end
-
- class HashOrIntegerValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless value.is_a?(Hash) || value.is_a?(Integer)
- record.errors.add(attribute, 'should be a hash or an integer')
- end
- end
- end
-
- class KeyValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- if validate_string(value)
- validate_path(record, attribute, value)
- else
- record.errors.add(attribute, 'should be a string or symbol')
- end
- end
-
- private
-
- def validate_path(record, attribute, value)
- path = CGI.unescape(value.to_s)
-
- if path.include?('/')
- record.errors.add(attribute, 'cannot contain the "/" character')
- elsif path == '.' || path == '..'
- record.errors.add(attribute, 'cannot be "." or ".."')
- end
- end
- end
-
- class RegexpValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_regexp(value)
- record.errors.add(attribute, 'must be a regular expression')
- end
- end
-
- private
-
- def look_like_regexp?(value)
- value.is_a?(String) && value.start_with?('/') &&
- value.end_with?('/')
- end
-
- def validate_regexp(value)
- look_like_regexp?(value) &&
- Regexp.new(value.to_s[1...-1]) &&
- true
- rescue RegexpError
- false
- end
- end
-
- class ArrayOfStringsOrRegexpsValidator < RegexpValidator
- def validate_each(record, attribute, value)
- unless validate_array_of_strings_or_regexps(value)
- record.errors.add(attribute, 'should be an array of strings or regexps')
- end
- end
-
- private
-
- def validate_array_of_strings_or_regexps(values)
- values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
- end
-
- def validate_string_or_regexp(value)
- return false unless value.is_a?(String)
- return validate_regexp(value) if look_like_regexp?(value)
-
- true
- end
- end
-
- class ArrayOfStringsOrStringValidator < RegexpValidator
- def validate_each(record, attribute, value)
- unless validate_array_of_strings_or_string(value)
- record.errors.add(attribute, 'should be an array of strings or a string')
- end
- end
-
- private
-
- def validate_array_of_strings_or_string(values)
- validate_array_of_strings(values) || validate_string(values)
- end
- end
-
- class TypeValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- type = options[:with]
- raise unless type.is_a?(Class)
-
- unless value.is_a?(type)
- message = options[:message] || "should be a #{type.name}"
- record.errors.add(attribute, message)
- end
- end
- end
-
- class VariablesValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_variables(value)
- record.errors.add(attribute, 'should be a hash of key value pairs')
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index 6fd3cec2f5f..89d790ebfa6 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents environment variables.
#
- class Variables < Node
- include Validatable
+ class Variables < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, variables: true
diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb
index 15ca47ef60e..ee4ea9bbb1d 100644
--- a/lib/gitlab/ci/config/external/file/base.rb
+++ b/lib/gitlab/ci/config/external/file/base.rb
@@ -37,8 +37,8 @@ module Gitlab
end
def to_hash
- @hash ||= Ci::Config::Loader.new(content).load!
- rescue Ci::Config::Loader::FormatError
+ @hash ||= Gitlab::Config::Loader::Yaml.new(content).load!
+ rescue Gitlab::Config::Loader::FormatError
nil
end
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index b445a872b3d..d33d1edfe35 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -16,6 +16,7 @@ module Gitlab
trigger_requests: Array(@command.trigger_request),
user: @command.current_user,
pipeline_schedule: @command.schedule,
+ merge_request: @command.merge_request,
protected: @command.protected_ref?,
variables_attributes: Array(@command.variables_attributes)
)
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 05978804d92..100b9521412 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -8,7 +8,7 @@ module Gitlab
Command = Struct.new(
:source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha,
- :trigger_request, :schedule,
+ :trigger_request, :schedule, :merge_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes
) do
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index c90976b2040..3b2cae07c12 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -19,15 +19,6 @@
# * review: REVIEW_DISABLED
# * stop_review: REVIEW_DISABLED
#
-# The sast and sast_dashboard jobs are executed to guarantee full compatibility
-# with the group security dashboard and the security reports with old runners.
-# If you use only runners with version 11.5 or above, you can disable the sast
-# job by setting the OLD_REPORTS_DISABLED environment variable. If you use only
-# runners with version below 11.5, you can disable the sast_dashboard job by
-# setting the NEW_REPORTS_DISABLED environment variable.
-# The sast_dashboard job will be removed in the future, when the sast job will
-# use the new reports syntax.
-#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
@@ -182,29 +173,6 @@ sast:
except:
variables:
- $SAST_DISABLED
- - $OLD_REPORTS_DISABLED
-
-sast_dashboard:
- stage: test
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - sast
- artifacts:
- reports:
- sast: gl-sast-report.json
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/
- except:
- variables:
- - $SAST_DISABLED
- - $NEW_REPORTS_DISABLED
dependency_scanning:
stage: test
@@ -658,6 +626,7 @@ rollout 100%:
fi
if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then
+ echo "Deploying first release with database initialization..."
helm upgrade --install \
--wait \
--set service.enabled="$service_enabled" \
@@ -680,6 +649,7 @@ rollout 100%:
"$name" \
chart/
+ echo "Deploying second release..."
helm upgrade --reuse-values \
--wait \
--set application.initializeCommand="" \
@@ -688,6 +658,7 @@ rollout 100%:
"$name" \
chart/
else
+ echo "Deploying new release..."
helm upgrade --install \
--wait \
--set service.enabled="$service_enabled" \
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index e6ec400e476..172926b8ab0 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -5,7 +5,7 @@ module Gitlab
class YamlProcessor
ValidationError = Class.new(StandardError)
- include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
+ include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :cache, :stages, :jobs
diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb
new file mode 100644
index 00000000000..560fe63df0e
--- /dev/null
+++ b/lib/gitlab/config/entry/attributable.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Attributable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def attributes(*attributes)
+ attributes.flatten.each do |attribute|
+ if method_defined?(attribute)
+ raise ArgumentError, 'Method already defined!'
+ end
+
+ define_method(attribute) do
+ return unless config.is_a?(Hash)
+
+ config[attribute]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/boolean.rb b/lib/gitlab/config/entry/boolean.rb
new file mode 100644
index 00000000000..1e8a57356e3
--- /dev/null
+++ b/lib/gitlab/config/entry/boolean.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents a boolean value.
+ #
+ class Boolean < Node
+ include Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb
new file mode 100644
index 00000000000..afdb60b2cd5
--- /dev/null
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This mixin is responsible for adding DSL, which purpose is to
+ # simplifly process of adding child nodes.
+ #
+ # This can be used only if parent node is a configuration entry that
+ # holds a hash as a configuration value, for example:
+ #
+ # job:
+ # script: ...
+ # artifacts: ...
+ #
+ module Configurable
+ extend ActiveSupport::Concern
+
+ included do
+ include Validatable
+
+ validations do
+ validates :config, type: Hash
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def compose!(deps = nil)
+ return unless valid?
+
+ self.class.nodes.each do |key, factory|
+ factory
+ .value(config[key])
+ .with(key: key, parent: self)
+
+ entries[key] = factory.create!
+ end
+
+ yield if block_given?
+
+ entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ class_methods do
+ def nodes
+ Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def entry(key, entry, metadata)
+ factory = ::Gitlab::Config::Entry::Factory.new(entry)
+ .with(description: metadata[:description])
+
+ (@nodes ||= {}).merge!(key.to_sym => factory)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def helpers(*nodes)
+ nodes.each do |symbol|
+ define_method("#{symbol}_defined?") do
+ entries[symbol]&.specified?
+ end
+
+ define_method("#{symbol}_value") do
+ return unless entries[symbol] && entries[symbol].valid?
+
+ entries[symbol].value
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
new file mode 100644
index 00000000000..30d43c9f9a1
--- /dev/null
+++ b/lib/gitlab/config/entry/factory.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Factory class responsible for fabricating entry objects.
+ #
+ class Factory
+ InvalidFactory = Class.new(StandardError)
+
+ def initialize(entry)
+ @entry = entry
+ @metadata = {}
+ @attributes = {}
+ end
+
+ def value(value)
+ @value = value
+ self
+ end
+
+ def metadata(metadata)
+ @metadata.merge!(metadata)
+ self
+ end
+
+ def with(attributes)
+ @attributes.merge!(attributes)
+ self
+ end
+
+ def create!
+ raise InvalidFactory unless defined?(@value)
+
+ ##
+ # We assume that unspecified entry is undefined.
+ # See issue #18775.
+ #
+ if @value.nil?
+ Entry::Unspecified.new(
+ fabricate_unspecified
+ )
+ else
+ fabricate(@entry, @value)
+ end
+ end
+
+ private
+
+ def fabricate_unspecified
+ ##
+ # If entry has a default value we fabricate concrete node
+ # with default value.
+ #
+ if @entry.default.nil?
+ fabricate(Entry::Undefined)
+ else
+ fabricate(@entry, @entry.default)
+ end
+ end
+
+ def fabricate(entry, value = nil)
+ entry.new(value, @metadata).tap do |node|
+ node.key = @attributes[:key]
+ node.parent = @attributes[:parent]
+ node.description = @attributes[:description]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb
new file mode 100644
index 00000000000..d3ab5625743
--- /dev/null
+++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module LegacyValidationHelpers
+ private
+
+ def validate_duration(value)
+ value.is_a?(String) && ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_duration_limit(value, limit)
+ return false unless value.is_a?(String)
+
+ ChronicDuration.parse(value).second.from_now <
+ ChronicDuration.parse(limit).second.from_now
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
+ end
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) &&
+ variables.flatten.all? do |value|
+ validate_string(value) || validate_integer(value)
+ end
+ end
+
+ def validate_integer(value)
+ value.is_a?(Integer)
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
+ end
+
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
+ def validate_string_or_regexp(value)
+ return true if value.is_a?(Symbol)
+ return false unless value.is_a?(String)
+
+ if value.first == '/' && value.last == '/'
+ validate_regexp(value[1...-1])
+ else
+ true
+ end
+ end
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
new file mode 100644
index 00000000000..30357b2c95b
--- /dev/null
+++ b/lib/gitlab/config/entry/node.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Base abstract class for each configuration entry node.
+ #
+ class Node
+ InvalidError = Class.new(StandardError)
+
+ attr_reader :config, :metadata
+ attr_accessor :key, :parent, :description
+
+ def initialize(config, **metadata)
+ @config = config
+ @metadata = metadata
+ @entries = {}
+
+ self.class.aspects.to_a.each do |aspect|
+ instance_exec(&aspect)
+ end
+ end
+
+ def [](key)
+ @entries[key] || Entry::Undefined.new
+ end
+
+ def compose!(deps = nil)
+ return unless valid?
+
+ yield if block_given?
+ end
+
+ def leaf?
+ @entries.none?
+ end
+
+ def descendants
+ @entries.values
+ end
+
+ def ancestors
+ @parent ? @parent.ancestors + [@parent] : []
+ end
+
+ def valid?
+ errors.none?
+ end
+
+ def errors
+ []
+ end
+
+ def value
+ if leaf?
+ @config
+ else
+ meaningful = @entries.select do |_key, value|
+ value.specified? && value.relevant?
+ end
+
+ Hash[meaningful.map { |key, entry| [key, entry.value] }]
+ end
+ end
+
+ def specified?
+ true
+ end
+
+ def relevant?
+ true
+ end
+
+ def location
+ name = @key.presence || self.class.name.to_s.demodulize
+ .underscore.humanize.downcase
+
+ ancestors.map(&:key).append(name).compact.join(':')
+ end
+
+ def inspect
+ val = leaf? ? config : descendants
+ unspecified = specified? ? '' : '(unspecified) '
+ "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
+ end
+
+ def self.default
+ end
+
+ def self.aspects
+ @aspects ||= []
+ end
+
+ private
+
+ attr_reader :entries
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
new file mode 100644
index 00000000000..3e148fe2e91
--- /dev/null
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ class Simplifiable < SimpleDelegator
+ EntryStrategy = Struct.new(:name, :condition)
+
+ def initialize(config, **metadata)
+ unless self.class.const_defined?(:UnknownStrategy)
+ raise ArgumentError, 'UndefinedStrategy not available!'
+ end
+
+ strategy = self.class.strategies.find do |variant|
+ variant.condition.call(config)
+ end
+
+ entry = self.class.entry_class(strategy)
+
+ super(entry.new(config, metadata))
+ end
+
+ def self.strategy(name, **opts)
+ EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
+ strategies.append(strategy)
+ end
+ end
+
+ def self.strategies
+ @strategies ||= []
+ end
+
+ def self.entry_class(strategy)
+ if strategy.present?
+ self.const_get(strategy.name)
+ else
+ self::UnknownStrategy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/undefined.rb b/lib/gitlab/config/entry/undefined.rb
new file mode 100644
index 00000000000..5f708abc80c
--- /dev/null
+++ b/lib/gitlab/config/entry/undefined.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This class represents an undefined entry.
+ #
+ class Undefined < Node
+ def initialize(*)
+ super(nil)
+ end
+
+ def value
+ nil
+ end
+
+ def valid?
+ true
+ end
+
+ def errors
+ []
+ end
+
+ def specified?
+ false
+ end
+
+ def relevant?
+ false
+ end
+
+ def inspect
+ "#<#{self.class.name}>"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/unspecified.rb b/lib/gitlab/config/entry/unspecified.rb
new file mode 100644
index 00000000000..c096180d0f8
--- /dev/null
+++ b/lib/gitlab/config/entry/unspecified.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This class represents an unspecified entry.
+ #
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
+ #
+ class Unspecified < SimpleDelegator
+ def specified?
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb
new file mode 100644
index 00000000000..1c88c68c11c
--- /dev/null
+++ b/lib/gitlab/config/entry/validatable.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Validatable
+ extend ActiveSupport::Concern
+
+ def self.included(node)
+ node.aspects.append -> do
+ @validator = self.class.validator.new(self)
+ @validator.validate(:new)
+ end
+ end
+
+ def errors
+ @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ class_methods do
+ def validator
+ @validator ||= Class.new(Entry::Validator).tap do |validator|
+ if defined?(@validations)
+ @validations.each { |rules| validator.class_eval(&rules) }
+ end
+ end
+ end
+
+ private
+
+ def validations(&block)
+ (@validations ||= []).append(block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validator.rb b/lib/gitlab/config/entry/validator.rb
new file mode 100644
index 00000000000..e5efd4a7b0a
--- /dev/null
+++ b/lib/gitlab/config/entry/validator.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ class Validator < SimpleDelegator
+ include ActiveModel::Validations
+ include Entry::Validators
+
+ def initialize(entry)
+ super(entry)
+ end
+
+ def messages
+ errors.full_messages.map do |error|
+ "#{location} #{error}".downcase
+ end
+ end
+
+ def self.name
+ 'Validator'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
new file mode 100644
index 00000000000..25bfa50f829
--- /dev/null
+++ b/lib/gitlab/config/entry/validators.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Validators
+ class AllowedKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unknown_keys = value.try(:keys).to_a - options[:in]
+
+ if unknown_keys.any?
+ record.errors.add(attribute, "contains unknown keys: " +
+ unknown_keys.join(', '))
+ end
+ end
+ end
+
+ class AllowedValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless options[:in].include?(value.to_s)
+ record.errors.add(attribute, "unknown value: #{value}")
+ end
+ end
+ end
+
+ class AllowedArrayValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unkown_values = value - options[:in]
+ unless unkown_values.empty?
+ record.errors.add(attribute, "contains unknown values: " +
+ unkown_values.join(', '))
+ end
+ end
+ end
+
+ class ArrayOfStringsValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array of strings')
+ end
+ end
+ end
+
+ class BooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_boolean(value)
+ record.errors.add(attribute, 'should be a boolean value')
+ end
+ end
+ end
+
+ class DurationValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_duration(value)
+ record.errors.add(attribute, 'should be a duration')
+ end
+
+ if options[:limit]
+ unless validate_duration_limit(value, options[:limit])
+ record.errors.add(attribute, 'should not exceed the limit')
+ end
+ end
+ end
+ end
+
+ class HashOrStringValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(String)
+ record.errors.add(attribute, 'should be a hash or a string')
+ end
+ end
+ end
+
+ class HashOrIntegerValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(Integer)
+ record.errors.add(attribute, 'should be a hash or an integer')
+ end
+ end
+ end
+
+ class KeyValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ if validate_string(value)
+ validate_path(record, attribute, value)
+ else
+ record.errors.add(attribute, 'should be a string or symbol')
+ end
+ end
+
+ private
+
+ def validate_path(record, attribute, value)
+ path = CGI.unescape(value.to_s)
+
+ if path.include?('/')
+ record.errors.add(attribute, 'cannot contain the "/" character')
+ elsif path == '.' || path == '..'
+ record.errors.add(attribute, 'cannot be "." or ".."')
+ end
+ end
+ end
+
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+
+ true
+ end
+ end
+
+ class ArrayOfStringsOrStringValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_string(value)
+ record.errors.add(attribute, 'should be an array of strings or a string')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_string(values)
+ validate_array_of_strings(values) || validate_string(values)
+ end
+ end
+
+ class TypeValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ type = options[:with]
+ raise unless type.is_a?(Class)
+
+ unless value.is_a?(type)
+ message = options[:message] || "should be a #{type.name}"
+ record.errors.add(attribute, message)
+ end
+ end
+ end
+
+ class VariablesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_variables(value)
+ record.errors.add(attribute, 'should be a hash of key value pairs')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/loader/format_error.rb b/lib/gitlab/config/loader/format_error.rb
new file mode 100644
index 00000000000..848ff96d201
--- /dev/null
+++ b/lib/gitlab/config/loader/format_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Loader
+ FormatError = Class.new(StandardError)
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/config/loader/yaml.rb
index b4c491e84a6..8159f8b8026 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -1,15 +1,13 @@
# frozen_string_literal: true
module Gitlab
- module Ci
- class Config
- class Loader
- FormatError = Class.new(StandardError)
-
+ module Config
+ module Loader
+ class Yaml
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e
- raise FormatError, e.message
+ raise Loader::FormatError, e.message
end
def valid?
@@ -18,7 +16,7 @@ module Gitlab
def load!
unless valid?
- raise FormatError, 'Invalid configuration format'
+ raise Loader::FormatError, 'Invalid configuration format'
end
@config.deep_symbolize_keys
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/crypto_helper.rb b/lib/gitlab/crypto_helper.rb
index 68d0b5d8f8a..87a03d9c58f 100644
--- a/lib/gitlab/crypto_helper.rb
+++ b/lib/gitlab/crypto_helper.rb
@@ -6,8 +6,8 @@ module Gitlab
AES256_GCM_OPTIONS = {
algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_truncated,
- iv: Settings.attr_encrypted_db_key_base_truncated[0..11]
+ key: Settings.attr_encrypted_db_key_base_32,
+ iv: Settings.attr_encrypted_db_key_base_12
}.freeze
def sha256(value)
@@ -17,7 +17,7 @@ module Gitlab
def aes256_gcm_encrypt(value)
encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value))
- Base64.encode64(encrypted_token)
+ Base64.strict_encode64(encrypted_token)
end
def aes256_gcm_decrypt(value)
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index ea6529e2dc4..f3d37ccd72a 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -1,7 +1,12 @@
# frozen_string_literal: true
# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
-# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
+# We can optimize this by using various strategies for approximate counting.
+#
+# For example, we can use the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
+#
+# However, since statistics are not always up to date, we also implement a table sampling strategy
+# that performs an exact count but only on a sample of the table. See TablesampleCountStrategy.
module Gitlab
module Database
module Count
@@ -20,68 +25,30 @@ module Gitlab
end
# Takes in an array of models and returns a Hash for the approximate
- # counts for them. If the model's table has not been vacuumed or
- # analyzed recently, simply run the Model.count to get the data.
+ # counts for them.
+ #
+ # Various count strategies can be specified that are executed in
+ # sequence until all tables have an approximate count attached
+ # or we run out of strategies.
+ #
+ # Note that not all strategies are available on all supported RDBMS.
#
# @param [Array]
# @return [Hash] of Model -> count mapping
- def self.approximate_counts(models)
- table_to_model_map = models.each_with_object({}) do |model, hash|
- hash[model.table_name] = model
- end
-
- table_names = table_to_model_map.keys
- counts_by_table_name = Gitlab::Database.postgresql? ? reltuples_from_recently_updated(table_names) : {}
+ def self.approximate_counts(models, strategies: [TablesampleCountStrategy, ReltuplesCountStrategy, ExactCountStrategy])
+ strategies.each_with_object({}) do |strategy, counts_by_model|
+ if strategy.enabled?
+ models_with_missing_counts = models - counts_by_model.keys
- # Convert table -> count to Model -> count
- counts_by_model = counts_by_table_name.each_with_object({}) do |pair, hash|
- model = table_to_model_map[pair.first]
- hash[model] = pair.second
- end
+ break counts_by_model if models_with_missing_counts.empty?
- missing_tables = table_names - counts_by_table_name.keys
+ counts = strategy.new(models_with_missing_counts).count
- missing_tables.each do |table|
- model = table_to_model_map[table]
- counts_by_model[model] = model.count
+ counts.each do |model, count|
+ counts_by_model[model] = count
+ end
+ end
end
-
- counts_by_model
- end
-
- # Returns a hash of the table names that have recently updated tuples.
- #
- # @param [Array] table names
- # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
- def self.reltuples_from_recently_updated(table_names)
- query = postgresql_estimate_query(table_names)
- rows = []
-
- # Querying tuple stats only works on the primary. Due to load
- # balancing, we need to ensure this query hits the load balancer. The
- # easiest way to do this is to start a transaction.
- ActiveRecord::Base.transaction do
- rows = ActiveRecord::Base.connection.select_all(query)
- end
-
- rows.each_with_object({}) { |row, data| data[row['table_name']] = row['estimate'].to_i }
- rescue *CONNECTION_ERRORS
- {}
- end
-
- # Generates the PostgreSQL query to return the tuples for tables
- # that have been vacuumed or analyzed in the last hour.
- #
- # @param [Array] table names
- # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
- def self.postgresql_estimate_query(table_names)
- time = "to_timestamp(#{1.hour.ago.to_i})"
- <<~SQL
- SELECT pg_class.relname AS table_name, reltuples::bigint AS estimate FROM pg_class
- LEFT JOIN pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
- WHERE pg_class.relname IN (#{table_names.map { |table| "'#{table}'" }.join(',')})
- AND (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
- SQL
end
end
end
diff --git a/lib/gitlab/database/count/exact_count_strategy.rb b/lib/gitlab/database/count/exact_count_strategy.rb
new file mode 100644
index 00000000000..fa6951eda22
--- /dev/null
+++ b/lib/gitlab/database/count/exact_count_strategy.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ # This strategy performs an exact count on the model.
+ #
+ # This is guaranteed to be accurate, however it also scans the
+ # whole table. Hence, there are no guarantees with respect
+ # to runtime.
+ #
+ # Note that for very large tables, this may even timeout.
+ class ExactCountStrategy
+ attr_reader :models
+ def initialize(models)
+ @models = models
+ end
+
+ def count
+ models.each_with_object({}) do |model, data|
+ data[model] = model.count
+ end
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ def self.enabled?
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count/reltuples_count_strategy.rb b/lib/gitlab/database/count/reltuples_count_strategy.rb
new file mode 100644
index 00000000000..c3a674aeb7e
--- /dev/null
+++ b/lib/gitlab/database/count/reltuples_count_strategy.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ class PgClass < ActiveRecord::Base
+ self.table_name = 'pg_class'
+ end
+
+ # This strategy counts based on PostgreSQL's statistics in pg_stat_user_tables.
+ #
+ # Specifically, it relies on the column reltuples in said table. An additional
+ # check is performed to make sure statistics were updated within the last hour.
+ #
+ # Otherwise, this strategy skips tables with outdated statistics.
+ #
+ # There are no guarantees with respect to the accuracy of this strategy. Runtime
+ # however is guaranteed to be "fast", because it only looks up statistics.
+ class ReltuplesCountStrategy
+ attr_reader :models
+ def initialize(models)
+ @models = models
+ end
+
+ # Returns a hash of the table names that have recently updated tuples.
+ #
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def count
+ size_estimates
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ def self.enabled?
+ Gitlab::Database.postgresql?
+ end
+
+ private
+
+ def table_names
+ models.map(&:table_name)
+ end
+
+ def size_estimates(check_statistics: true)
+ table_to_model = models.each_with_object({}) { |model, h| h[model.table_name] = model }
+
+ # Querying tuple stats only works on the primary. Due to load balancing, the
+ # easiest way to do this is to start a transaction.
+ ActiveRecord::Base.transaction do
+ get_statistics(table_names, check_statistics: check_statistics).each_with_object({}) do |row, data|
+ model = table_to_model[row.table_name]
+ data[model] = row.estimate
+ end
+ end
+ end
+
+ # Generates the PostgreSQL query to return the tuples for tables
+ # that have been vacuumed or analyzed in the last hour.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def get_statistics(table_names, check_statistics: true)
+ time = 1.hour.ago
+
+ query = PgClass.joins("LEFT JOIN pg_stat_user_tables USING (relname)")
+ .where(relname: table_names)
+ .select('pg_class.relname AS table_name, reltuples::bigint AS estimate')
+
+ if check_statistics
+ query = query.where('last_vacuum > ? OR last_autovacuum > ? OR last_analyze > ? OR last_autoanalyze > ?',
+ time, time, time, time)
+ end
+
+ query
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count/tablesample_count_strategy.rb b/lib/gitlab/database/count/tablesample_count_strategy.rb
new file mode 100644
index 00000000000..cf1cf054dbf
--- /dev/null
+++ b/lib/gitlab/database/count/tablesample_count_strategy.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ # A tablesample count executes in two phases:
+ # * Estimate table sizes based on reltuples.
+ # * Based on the estimate:
+ # * If the table is considered 'small', execute an exact relation count.
+ # * Otherwise, count on a sample of the table using TABLESAMPLE.
+ #
+ # The size of the sample is chosen in a way that we always roughly scan
+ # the same amount of rows (see TABLESAMPLE_ROW_TARGET).
+ #
+ # There are no guarantees with respect to the accuracy of the result or runtime.
+ class TablesampleCountStrategy < ReltuplesCountStrategy
+ EXACT_COUNT_THRESHOLD = 10_000
+ TABLESAMPLE_ROW_TARGET = 10_000
+
+ def count
+ estimates = size_estimates(check_statistics: false)
+
+ models.each_with_object({}) do |model, count_by_model|
+ count = perform_count(model, estimates[model])
+ count_by_model[model] = count if count
+ end
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ def self.enabled?
+ Gitlab::Database.postgresql? && Feature.enabled?(:tablesample_counts)
+ end
+
+ private
+
+ def perform_count(model, estimate)
+ # If we estimate 0, we may not have statistics at all. Don't use them.
+ return nil unless estimate && estimate > 0
+
+ if estimate < EXACT_COUNT_THRESHOLD
+ # The table is considered small, the assumption here is that
+ # the exact count will be fast anyways.
+ model.count
+ else
+ # The table is considered large, let's only count on a sample.
+ tablesample_count(model, estimate)
+ end
+ end
+
+ def tablesample_count(model, estimate)
+ portion = (TABLESAMPLE_ROW_TARGET.to_f / estimate).round(4)
+ inverse = 1 / portion
+ query = <<~SQL
+ SELECT (COUNT(*)*#{inverse})::integer AS count
+ FROM #{model.table_name} TABLESAMPLE SYSTEM (#{portion * 100})
+ SQL
+
+ rows = ActiveRecord::Base.connection.select_all(query)
+
+ Integer(rows.first['count'])
+ end
+ end
+ end
+ end
+end
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/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 10df037a0dd..c5bbf522f7c 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -34,6 +34,16 @@ module Gitlab
@diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) }
end
+ # This mutates `diff_files` lines.
+ def unfold_diff_files(positions)
+ positions_grouped_by_path = positions.group_by { |position| position.file_path }
+
+ diff_files.each do |diff_file|
+ positions = positions_grouped_by_path.fetch(diff_file.file_path, [])
+ positions.each { |position| diff_file.unfold_diff_lines(position) }
+ end
+ end
+
def diff_file_with_old_path(old_path)
diff_files.find { |diff_file| diff_file.old_path == old_path }
end
diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb
index 586c5cf87af..663bad95db7 100644
--- a/lib/gitlab/diff/file_collection/compare.rb
+++ b/lib/gitlab/diff/file_collection/compare.rb
@@ -10,6 +10,10 @@ module Gitlab
diff_options: diff_options,
diff_refs: diff_refs)
end
+
+ def unfold_diff_lines(positions)
+ # no-op
+ end
end
end
end
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/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..255601382b1 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['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/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index c32c2c0b2fb..22d2d149e65 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -385,7 +385,8 @@ module Gitlab
file_path: encode_binary(action[:file_path]),
previous_path: encode_binary(action[:previous_path]),
base64_content: action[:encoding] == 'base64',
- execute_filemode: !!action[:execute_filemode]
+ execute_filemode: !!action[:execute_filemode],
+ infer_content: !!action[:infer_content]
)
rescue RangeError
raise ArgumentError, "Unknown action '#{action[:action]}'"
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 374dc9d3c00..bc3ea9e9226 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -80,7 +80,7 @@ module Gitlab
end
def fail_import(message)
- project.mark_import_as_failed(message)
+ project.import_state.mark_as_failed(message)
false
end
end
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
index a77ac1e4fa6..9d81441d96e 100644
--- a/lib/gitlab/github_import/parallel_importer.rb
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -41,8 +41,7 @@ module Gitlab
Gitlab::SidekiqStatus
.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
- project.ensure_import_state
- project.import_state&.update_column(:jid, jid)
+ project.import_state.update_column(:jid, jid)
Stage::ImportRepositoryWorker
.perform_async(project.id)
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/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index c940ea7305e..97cbdc6cb39 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -34,8 +34,8 @@ module Gitlab
# reached. So all ancestors *lower* than the specified ancestor will be
# included.
# rubocop: disable CodeReuse/ActiveRecord
- def ancestors(upto: nil)
- base_and_ancestors(upto: upto).where.not(id: ancestors_base.select(:id))
+ def ancestors(upto: nil, hierarchy_order: nil)
+ base_and_ancestors(upto: upto, hierarchy_order: hierarchy_order).where.not(id: ancestors_base.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -45,11 +45,22 @@ module Gitlab
# Passing an `upto` will stop the recursion once the specified parent_id is
# reached. So all ancestors *lower* than the specified acestor will be
# included.
- def base_and_ancestors(upto: nil)
+ #
+ # Passing a `hierarchy_order` with either `:asc` or `:desc` will cause the
+ # recursive query order from most nested group to root or from the root
+ # ancestor to most nested group respectively. This uses a `depth` column
+ # where `1` is defined as the depth for the base and increment as we go up
+ # each parent.
+ # rubocop: disable CodeReuse/ActiveRecord
+ def base_and_ancestors(upto: nil, hierarchy_order: nil)
return ancestors_base unless Group.supports_nested_groups?
- read_only(base_and_ancestors_cte(upto).apply_to(model.all))
+ recursive_query = base_and_ancestors_cte(upto, hierarchy_order).apply_to(model.all)
+ recursive_query = recursive_query.order(depth: hierarchy_order) if hierarchy_order
+
+ read_only(recursive_query)
end
+ # rubocop: enable CodeReuse/ActiveRecord
# Returns a relation that includes the descendants_base set of groups
# and all their descendants (recursively).
@@ -107,16 +118,22 @@ module Gitlab
private
# rubocop: disable CodeReuse/ActiveRecord
- def base_and_ancestors_cte(stop_id = nil)
+ def base_and_ancestors_cte(stop_id = nil, hierarchy_order = nil)
cte = SQL::RecursiveCTE.new(:base_and_ancestors)
+ depth_column = :depth
+
+ base_query = ancestors_base.except(:order)
+ base_query = base_query.select("1 as #{depth_column}", groups_table[Arel.star]) if hierarchy_order
- cte << ancestors_base.except(:order)
+ cte << base_query
# Recursively get all the ancestors of the base set.
parent_query = model
.from([groups_table, cte.table])
.where(groups_table[:id].eq(cte.table[:parent_id]))
.except(:order)
+
+ parent_query = parent_query.select(cte.table[depth_column] + 1, groups_table[Arel.star]) if hierarchy_order
parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
cte << parent_query
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index b40eac3de9a..d10d4f2f746 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -51,7 +51,7 @@ project_tree:
- resource_label_events:
- label:
:priorities
- - pipelines:
+ - ci_pipelines:
- notes:
- :author
- events:
@@ -98,13 +98,12 @@ excluded_attributes:
- :avatar
- :import_type
- :import_source
- - :import_error
- :mirror
- :runners_token
+ - :runners_token_encrypted
- :repository_storage
- :repository_read_only
- :lfs_enabled
- - :import_jid
- :created_at
- :updated_at
- :id
@@ -116,6 +115,13 @@ excluded_attributes:
- :remote_mirror_available_overridden
- :description_html
- :repository_languages
+ - :bfg_object_map
+ namespaces:
+ - :runners_token
+ - :runners_token_encrypted
+ project_import_state:
+ - :last_error
+ - :jid
prometheus_metrics:
- :common
- :identifier
@@ -137,6 +143,7 @@ excluded_attributes:
statuses:
- :trace
- :token
+ - :token_encrypted
- :when
- :artifacts_file
- :artifacts_metadata
@@ -154,6 +161,9 @@ excluded_attributes:
- :encrypted_token_iv
- :encrypted_url
- :encrypted_url_iv
+ runners:
+ - :token
+ - :token_encrypted
services:
- :template
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 8cd4efd91cc..a56ec65b9f1 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -26,6 +26,8 @@ module Gitlab
@project_members = @tree_hash.delete('project_members')
+ RelationRenameService.rename(@tree_hash)
+
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
create_relations
@@ -214,7 +216,7 @@ module Gitlab
end
def nil_iid_pipeline?(relation_key, relation_item)
- relation_key == 'pipelines' && relation_item['iid'].nil?
+ relation_key == 'ci_pipelines' && relation_item['iid'].nil?
end
end
end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 29f2dc80813..2255635acdf 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -34,6 +34,8 @@ module Gitlab
project_json['project_members'] += group_members_json
+ RelationRenameService.add_new_associations(project_json)
+
project_json.to_json
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 097c7653754..a4902e2104f 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -4,12 +4,14 @@ module Gitlab
module ImportExport
class RelationFactory
OVERRIDES = { snippets: :project_snippets,
+ ci_pipelines: 'Ci::Pipeline',
pipelines: 'Ci::Pipeline',
stages: 'Ci::Stage',
statuses: 'commit_status',
triggers: 'Ci::Trigger',
pipeline_schedules: 'Ci::PipelineSchedule',
builds: 'Ci::Build',
+ runners: 'Ci::Runner',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
@@ -33,7 +35,7 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze
- TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
+ TOKEN_RESET_MODELS = %w[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
def self.create(*args)
new(*args).create
diff --git a/lib/gitlab/import_export/relation_rename_service.rb b/lib/gitlab/import_export/relation_rename_service.rb
new file mode 100644
index 00000000000..179bde5e21e
--- /dev/null
+++ b/lib/gitlab/import_export/relation_rename_service.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+# This class is intended to help with relation renames within Gitlab versions
+# and allow compatibility between versions.
+# If you have to change one relationship name that is imported/exported,
+# you should add it to the RENAMES constant indicating the old name and the
+# new one.
+# The behavior of these renamed relationships should be transient and it should
+# only last one release until you completely remove the renaming from the list.
+#
+# When importing, this class will check the project hash and:
+# - if only the old relationship name is found, it will rename it with the new one
+# - if only the new relationship name is found, it will do nothing
+# - if it finds both, it will use the new relationship data
+#
+# When exporting, this class will duplicate the keys in the resulting file.
+# This way, if we open the file in an old version of the exporter it will work
+# and also it will with the newer versions.
+module Gitlab
+ module ImportExport
+ class RelationRenameService
+ RENAMES = {
+ 'pipelines' => 'ci_pipelines' # Added in 11.6, remove in 11.7
+ }.freeze
+
+ def self.rename(tree_hash)
+ return unless tree_hash&.present?
+
+ RENAMES.each do |old_name, new_name|
+ old_entry = tree_hash.delete(old_name)
+
+ next if tree_hash[new_name]
+ next unless old_entry
+
+ tree_hash[new_name] = old_entry
+ end
+ end
+
+ def self.add_new_associations(tree_hash)
+ RENAMES.each do |old_name, new_name|
+ next if tree_hash.key?(old_name)
+
+ tree_hash[old_name] = tree_hash[new_name]
+ end
+ end
+ 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/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 3748fd6b5ef..a9957a85d48 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -85,6 +85,8 @@ module Gitlab
end
def to_kubeconfig(url:, namespace:, token:, ca_pem: nil)
+ return unless token.present?
+
config = {
apiVersion: 'v1',
clusters: [
@@ -113,7 +115,7 @@ module Gitlab
kubeconfig_embed_ca_pem(config, ca_pem) if ca_pem
- config.deep_stringify_keys
+ YAML.dump(config.deep_stringify_keys)
end
private
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index b947f6b551e..fe839940f74 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -46,6 +46,7 @@ module Gitlab
:create_secret,
:create_service_account,
:update_config_map,
+ :update_secret,
:update_service_account,
to: :core_client
@@ -80,8 +81,64 @@ module Gitlab
@kubeclient_options = kubeclient_options
end
+ def create_or_update_cluster_role_binding(resource)
+ if cluster_role_binding_exists?(resource)
+ update_cluster_role_binding(resource)
+ else
+ create_cluster_role_binding(resource)
+ end
+ end
+
+ def create_or_update_role_binding(resource)
+ if role_binding_exists?(resource)
+ update_role_binding(resource)
+ else
+ create_role_binding(resource)
+ end
+ end
+
+ def create_or_update_service_account(resource)
+ if service_account_exists?(resource)
+ update_service_account(resource)
+ else
+ create_service_account(resource)
+ end
+ end
+
+ def create_or_update_secret(resource)
+ if secret_exists?(resource)
+ update_secret(resource)
+ else
+ create_secret(resource)
+ end
+ end
+
private
+ def cluster_role_binding_exists?(resource)
+ get_cluster_role_binding(resource.metadata.name)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
+ end
+
+ def role_binding_exists?(resource)
+ get_role_binding(resource.metadata.name, resource.metadata.namespace)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
+ end
+
+ def service_account_exists?(resource)
+ get_service_account(resource.metadata.name, resource.metadata.namespace)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
+ end
+
+ def secret_exists?(resource)
+ get_secret(resource.metadata.name, resource.metadata.namespace)
+ rescue ::Kubeclient::ResourceNotFoundError
+ false
+ end
+
def build_kubeclient(api_group, api_version)
::Kubeclient::Client.new(
join_api_url(api_prefix, api_group),
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index 43695451b87..c526d31a591 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -80,8 +80,7 @@ module Gitlab
def handle_errors
return unless errors.any?
- project.ensure_import_state
- project.import_state&.update_column(:last_error, {
+ project.import_state.update_column(:last_error, {
message: 'The remote data could not be fully imported.',
errors: errors
}.to_json)
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index fa44bd842b2..05d3096a208 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -38,7 +38,7 @@ module Gitlab
end
def type
- actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+ user? ? :lfs_token : :lfs_deploy_token
end
def actor_name
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/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 86efe8ad114..b8040f73cee 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'resolv'
+require 'ipaddress'
module Gitlab
class UrlBlocker
@@ -10,11 +11,8 @@ module Gitlab
def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: [])
return true if url.nil?
- begin
- uri = Addressable::URI.parse(url)
- rescue Addressable::URI::InvalidURIError
- raise BlockedUrlError, "URI is invalid"
- end
+ # Param url can be a string, URI or Addressable::URI
+ uri = parse_url(url)
# Allow imports from the GitLab instance itself but only from the configured ports
return true if internal?(uri)
@@ -26,7 +24,9 @@ module Gitlab
validate_hostname!(uri.hostname)
begin
- addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM)
+ addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
+ addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
+ end
rescue SocketError
return true
end
@@ -49,6 +49,18 @@ module Gitlab
private
+ def parse_url(url)
+ raise Addressable::URI::InvalidURIError if multiline?(url)
+
+ Addressable::URI.parse(url)
+ rescue Addressable::URI::InvalidURIError, URI::InvalidURIError
+ raise BlockedUrlError, 'URI is invalid'
+ end
+
+ def multiline?(url)
+ CGI.unescape(url.to_s) =~ /\n|\r/
+ end
+
def validate_port!(port, ports)
return if port.blank?
# Only ports under 1024 are restricted
@@ -73,13 +85,14 @@ module Gitlab
def validate_hostname!(value)
return if value.blank?
+ return if IPAddress.valid?(value)
return if value =~ /\A\p{Alnum}/
- raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
+ raise BlockedUrlError, "Hostname or IP address invalid"
end
def validate_localhost!(addrs_info)
- local_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+ local_ips = ["::", "0.0.0.0"]
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
return if (local_ips & addrs_info.map(&:ip_address)).empty?
@@ -94,7 +107,7 @@ module Gitlab
end
def validate_local_network!(addrs_info)
- return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
+ return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? || addr.ipv6_unique_local? }
raise BlockedUrlError, "Requests to the local network are not allowed"
end
@@ -111,12 +124,14 @@ module Gitlab
end
def internal_web?(uri)
- uri.hostname == config.gitlab.host &&
+ uri.scheme == config.gitlab.protocol &&
+ uri.hostname == config.gitlab.host &&
(uri.port.blank? || uri.port == config.gitlab.port)
end
def internal_shell?(uri)
- uri.hostname == config.gitlab_shell.ssh_host &&
+ uri.scheme == 'ssh' &&
+ uri.hostname == config.gitlab_shell.ssh_host &&
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 9bceec749fc..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 }
@@ -55,7 +57,11 @@ module Gitlab
environments: count(::Environment),
clusters: count(::Clusters::Cluster),
clusters_enabled: count(::Clusters::Cluster.enabled),
+ project_clusters_enabled: count(::Clusters::Cluster.enabled.project_type),
+ group_clusters_enabled: count(::Clusters::Cluster.enabled.group_type),
clusters_disabled: count(::Clusters::Cluster.disabled),
+ project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
+ group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
clusters_applications_helm: count(::Clusters::Applications::Helm.installed),
@@ -69,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')),
@@ -82,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
@@ -160,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 9e59137a2c0..26fc56227a2 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>]
@@ -16,6 +25,21 @@ module Gitlab
str.force_encoding(Encoding::UTF_8)
end
+ def ensure_utf8_size(str, bytes:)
+ raise ArgumentError, 'Empty string provided!' if str.empty?
+ raise ArgumentError, 'Negative string size provided!' if bytes.negative?
+
+ truncated = str.each_char.each_with_object(+'') do |char, object|
+ if object.bytesize + char.bytesize > bytes
+ break object
+ else
+ object.concat(char)
+ end
+ end
+
+ truncated + ('0' * (bytes - truncated.bytesize))
+ end
+
# Append path to host, making sure there's one single / in between
def append_path(host, path)
"#{host.to_s.sub(%r{\/+$}, '')}/#{path.to_s.sub(%r{^\/+}, '')}"
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..da22ea9cf5c 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
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/system_check/gitaly_check.rb b/lib/system_check/gitaly_check.rb
new file mode 100644
index 00000000000..3d2517a7aca
--- /dev/null
+++ b/lib/system_check/gitaly_check.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ class GitalyCheck < BaseCheck
+ set_name 'Gitaly:'
+
+ def multi_check
+ Gitlab::HealthChecks::GitalyCheck.readiness.each do |result|
+ $stdout.print "#{result.labels[:shard]} ... "
+
+ if result.success
+ $stdout.puts 'OK'.color(:green)
+ else
+ $stdout.puts "FAIL: #{result.message}".color(:red)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/gitlab_shell_check.rb b/lib/system_check/gitlab_shell_check.rb
new file mode 100644
index 00000000000..31c4ec33247
--- /dev/null
+++ b/lib/system_check/gitlab_shell_check.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ # Used by gitlab:gitlab_shell:check rake task
+ class GitlabShellCheck < BaseCheck
+ set_name 'GitLab Shell:'
+
+ def multi_check
+ check_gitlab_shell
+ check_gitlab_shell_self_test
+ end
+
+ private
+
+ def check_gitlab_shell
+ required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
+ current_version = Gitlab::VersionInfo.parse(gitlab_shell_version)
+
+ $stdout.print "GitLab Shell version >= #{required_version} ? ... "
+ if current_version.valid? && required_version <= current_version
+ $stdout.puts "OK (#{current_version})".color(:green)
+ else
+ $stdout.puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
+ end
+ end
+
+ def check_gitlab_shell_self_test
+ gitlab_shell_repo_base = gitlab_shell_path
+ check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base)
+ $stdout.puts "Running #{check_cmd}"
+
+ if system(check_cmd, chdir: gitlab_shell_repo_base)
+ $stdout.puts 'gitlab-shell self-check successful'.color(:green)
+ else
+ $stdout.puts 'gitlab-shell self-check failed'.color(:red)
+ try_fixing_it(
+ 'Make sure GitLab is running;',
+ 'Check the gitlab-shell configuration file:',
+ sudo_gitlab("editor #{File.expand_path('config.yml', gitlab_shell_repo_base)}")
+ )
+ fix_and_rerun
+ end
+ end
+
+ # Helper methods
+ ########################
+
+ def gitlab_shell_path
+ Gitlab.config.gitlab_shell.path
+ end
+
+ def gitlab_shell_version
+ Gitlab::Shell.new.version
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email_check.rb b/lib/system_check/incoming_email_check.rb
new file mode 100644
index 00000000000..155b6547595
--- /dev/null
+++ b/lib/system_check/incoming_email_check.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ # Used by gitlab:incoming_email:check rake task
+ class IncomingEmailCheck < BaseCheck
+ set_name 'Incoming Email:'
+
+ def multi_check
+ if Gitlab.config.incoming_email.enabled
+ checks = [
+ SystemCheck::IncomingEmail::ImapAuthenticationCheck
+ ]
+
+ if Rails.env.production?
+ checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
+ checks << SystemCheck::IncomingEmail::MailRoomRunningCheck
+ else
+ checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck
+ end
+
+ SystemCheck.run('Reply by email', checks)
+ else
+ $stdout.puts 'Reply by email is disabled in config/gitlab.yml'
+ end
+ end
+ end
+end
diff --git a/lib/system_check/ldap_check.rb b/lib/system_check/ldap_check.rb
new file mode 100644
index 00000000000..619fb3cccb8
--- /dev/null
+++ b/lib/system_check/ldap_check.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ # Used by gitlab:ldap:check rake task
+ class LdapCheck < BaseCheck
+ set_name 'LDAP:'
+
+ def multi_check
+ if Gitlab::Auth::LDAP::Config.enabled?
+ # Only show up to 100 results because LDAP directories can be very big.
+ # This setting only affects the `rake gitlab:check` script.
+ limit = ENV['LDAP_CHECK_LIMIT']
+ limit = 100 if limit.blank?
+
+ check_ldap(limit)
+ else
+ $stdout.puts 'LDAP is disabled in config/gitlab.yml'
+ end
+ end
+
+ private
+
+ def check_ldap(limit)
+ servers = Gitlab::Auth::LDAP::Config.providers
+
+ servers.each do |server|
+ $stdout.puts "Server: #{server}"
+
+ begin
+ Gitlab::Auth::LDAP::Adapter.open(server) do |adapter|
+ check_ldap_auth(adapter)
+
+ $stdout.puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
+
+ users = adapter.users(adapter.config.uid, '*', limit)
+ users.each do |user|
+ $stdout.puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+ end
+ end
+ rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
+ $stdout.puts "Could not connect to the LDAP server: #{e.message}".color(:red)
+ end
+ end
+ end
+
+ def check_ldap_auth(adapter)
+ auth = adapter.config.has_auth?
+
+ message = if auth && adapter.ldap.bind
+ 'Success'.color(:green)
+ elsif auth
+ 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
+
+ $stdout.puts "LDAP authentication... #{message}"
+ end
+ end
+end
diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb
index ef8fe945f61..33020417e95 100644
--- a/lib/system_check/orphans/repository_check.rb
+++ b/lib/system_check/orphans/repository_check.rb
@@ -4,7 +4,6 @@ module SystemCheck
module Orphans
class RepositoryCheck < SystemCheck::BaseCheck
set_name 'Orphaned repositories:'
- attr_accessor :orphans
def multi_check
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
diff --git a/lib/system_check/rake_task/app_task.rb b/lib/system_check/rake_task/app_task.rb
new file mode 100644
index 00000000000..cc32feb8604
--- /dev/null
+++ b/lib/system_check/rake_task/app_task.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:app:check rake task
+ module AppTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'GitLab App'
+ end
+
+ def self.checks
+ [
+ SystemCheck::App::GitConfigCheck,
+ SystemCheck::App::DatabaseConfigExistsCheck,
+ SystemCheck::App::MigrationsAreUpCheck,
+ SystemCheck::App::OrphanedGroupMembersCheck,
+ SystemCheck::App::GitlabConfigExistsCheck,
+ SystemCheck::App::GitlabConfigUpToDateCheck,
+ SystemCheck::App::LogWritableCheck,
+ SystemCheck::App::TmpWritableCheck,
+ SystemCheck::App::UploadsDirectoryExistsCheck,
+ SystemCheck::App::UploadsPathPermissionCheck,
+ SystemCheck::App::UploadsPathTmpPermissionCheck,
+ SystemCheck::App::InitScriptExistsCheck,
+ SystemCheck::App::InitScriptUpToDateCheck,
+ SystemCheck::App::ProjectsHaveNamespaceCheck,
+ SystemCheck::App::RedisVersionCheck,
+ SystemCheck::App::RubyVersionCheck,
+ SystemCheck::App::GitVersionCheck,
+ SystemCheck::App::GitUserDefaultSSHConfigCheck,
+ SystemCheck::App::ActiveUsersCheck
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/gitaly_task.rb b/lib/system_check/rake_task/gitaly_task.rb
new file mode 100644
index 00000000000..0c3f694f98a
--- /dev/null
+++ b/lib/system_check/rake_task/gitaly_task.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:gitaly:check rake task
+ class GitalyTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Gitaly'
+ end
+
+ def self.checks
+ [SystemCheck::GitalyCheck]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/gitlab_shell_task.rb b/lib/system_check/rake_task/gitlab_shell_task.rb
new file mode 100644
index 00000000000..120e984c68b
--- /dev/null
+++ b/lib/system_check/rake_task/gitlab_shell_task.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:gitlab_shell:check rake task
+ class GitlabShellTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'GitLab Shell'
+ end
+
+ def self.checks
+ [SystemCheck::GitlabShellCheck]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/gitlab_task.rb b/lib/system_check/rake_task/gitlab_task.rb
new file mode 100644
index 00000000000..7ff36fd6eb5
--- /dev/null
+++ b/lib/system_check/rake_task/gitlab_task.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:check rake task
+ class GitlabTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'GitLab'
+ end
+
+ def self.manual_run_checks!
+ start_checking("#{name} subtasks")
+
+ subtasks.each(&:run_checks!)
+
+ finished_checking("#{name} subtasks")
+ end
+
+ def self.subtasks
+ [
+ SystemCheck::RakeTask::GitlabShellTask,
+ SystemCheck::RakeTask::GitalyTask,
+ SystemCheck::RakeTask::SidekiqTask,
+ SystemCheck::RakeTask::IncomingEmailTask,
+ SystemCheck::RakeTask::LdapTask,
+ SystemCheck::RakeTask::AppTask
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/incoming_email_task.rb b/lib/system_check/rake_task/incoming_email_task.rb
new file mode 100644
index 00000000000..c296c46feab
--- /dev/null
+++ b/lib/system_check/rake_task/incoming_email_task.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:incoming_email:check rake task
+ class IncomingEmailTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Incoming Email'
+ end
+
+ def self.checks
+ [SystemCheck::IncomingEmailCheck]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/ldap_task.rb b/lib/system_check/rake_task/ldap_task.rb
new file mode 100644
index 00000000000..03a180b9dfb
--- /dev/null
+++ b/lib/system_check/rake_task/ldap_task.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:ldap:check rake task
+ class LdapTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'LDAP'
+ end
+
+ def self.checks
+ [SystemCheck::LdapCheck]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/orphans/namespace_task.rb b/lib/system_check/rake_task/orphans/namespace_task.rb
new file mode 100644
index 00000000000..2822da45bc1
--- /dev/null
+++ b/lib/system_check/rake_task/orphans/namespace_task.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ module Orphans
+ # Used by gitlab:orphans:check_namespaces rake task
+ class NamespaceTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Orphans'
+ end
+
+ def self.checks
+ [SystemCheck::Orphans::NamespaceCheck]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/orphans/repository_task.rb b/lib/system_check/rake_task/orphans/repository_task.rb
new file mode 100644
index 00000000000..f14b3af22e8
--- /dev/null
+++ b/lib/system_check/rake_task/orphans/repository_task.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ module Orphans
+ # Used by gitlab:orphans:check_repositories rake task
+ class RepositoryTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Orphans'
+ end
+
+ def self.checks
+ [SystemCheck::Orphans::RepositoryCheck]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/orphans_task.rb b/lib/system_check/rake_task/orphans_task.rb
new file mode 100644
index 00000000000..31f8ede25e0
--- /dev/null
+++ b/lib/system_check/rake_task/orphans_task.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:orphans:check rake task
+ class OrphansTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Orphans'
+ end
+
+ def self.checks
+ [
+ SystemCheck::Orphans::NamespaceCheck,
+ SystemCheck::Orphans::RepositoryCheck
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/rake_task_helpers.rb b/lib/system_check/rake_task/rake_task_helpers.rb
new file mode 100644
index 00000000000..95f2a34a719
--- /dev/null
+++ b/lib/system_check/rake_task/rake_task_helpers.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Provides the run! method intended to be called from system check rake tasks
+ module RakeTaskHelpers
+ include ::SystemCheck::Helpers
+
+ def run!
+ warn_user_is_not_gitlab
+
+ if self.respond_to?(:manual_run_checks!)
+ manual_run_checks!
+ else
+ run_checks!
+ end
+ end
+
+ def run_checks!
+ SystemCheck.run(name, checks)
+ end
+
+ def name
+ raise NotImplementedError
+ end
+
+ def checks
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/sidekiq_task.rb b/lib/system_check/rake_task/sidekiq_task.rb
new file mode 100644
index 00000000000..3ccf009d4b9
--- /dev/null
+++ b/lib/system_check/rake_task/sidekiq_task.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module RakeTask
+ # Used by gitlab:sidekiq:check rake task
+ class SidekiqTask
+ extend RakeTaskHelpers
+
+ def self.name
+ 'Sidekiq'
+ end
+
+ def self.checks
+ [SystemCheck::SidekiqCheck]
+ end
+ end
+ end
+end
diff --git a/lib/system_check/sidekiq_check.rb b/lib/system_check/sidekiq_check.rb
new file mode 100644
index 00000000000..2f5fc2cea89
--- /dev/null
+++ b/lib/system_check/sidekiq_check.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ # Used by gitlab:sidekiq:check rake task
+ class SidekiqCheck < BaseCheck
+ set_name 'Sidekiq:'
+
+ def multi_check
+ check_sidekiq_running
+ only_one_sidekiq_running
+ end
+
+ private
+
+ def check_sidekiq_running
+ $stdout.print "Running? ... "
+
+ if sidekiq_process_count > 0
+ $stdout.puts "yes".color(:green)
+ else
+ $stdout.puts "no".color(:red)
+ try_fixing_it(
+ sudo_gitlab("RAILS_ENV=production bin/background_jobs start")
+ )
+ for_more_information(
+ see_installation_guide_section("Install Init Script"),
+ "see log/sidekiq.log for possible errors"
+ )
+ fix_and_rerun
+ end
+ end
+
+ def only_one_sidekiq_running
+ process_count = sidekiq_process_count
+ return if process_count.zero?
+
+ $stdout.print 'Number of Sidekiq processes ... '
+
+ if process_count == 1
+ $stdout.puts '1'.color(:green)
+ else
+ $stdout.puts "#{process_count}".color(:red)
+ try_fixing_it(
+ 'sudo service gitlab stop',
+ "sudo pkill -u #{gitlab_user} -f sidekiq",
+ "sleep 10 && sudo pkill -9 -u #{gitlab_user} -f sidekiq",
+ 'sudo service gitlab start'
+ )
+ fix_and_rerun
+ end
+ end
+
+ def sidekiq_process_count
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
+ ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
+ end
+ end
+end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index a497d26312e..2235a6ba194 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -82,7 +82,7 @@ namespace :gettext do
# `gettext:find` writes touches to temp files to `stderr` which would cause
# `static-analysis` to report failures. We can ignore these.
- silence_sdterr do
+ silence_stderr do
Rake::Task['gettext:find'].invoke
end
@@ -119,7 +119,7 @@ namespace :gettext do
end
end
- def silence_sdterr(&block)
+ def silence_stderr(&block)
old_stderr = $stderr.dup
$stderr.reopen(File::NULL)
$stderr.sync = true
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index a2c3e32948f..b594f150c3b 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -1,299 +1,66 @@
namespace :gitlab do
desc 'GitLab | Check the configuration of GitLab and its environment'
- task check: %w{gitlab:gitlab_shell:check
- gitlab:gitaly:check
- gitlab:sidekiq:check
- gitlab:incoming_email:check
- gitlab:ldap:check
- gitlab:app:check}
+ task check: :gitlab_environment do
+ SystemCheck::RakeTask::GitlabTask.run!
+ end
namespace :app do
desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :gitlab_environment do
- warn_user_is_not_gitlab
-
- checks = [
- SystemCheck::App::GitConfigCheck,
- SystemCheck::App::DatabaseConfigExistsCheck,
- SystemCheck::App::MigrationsAreUpCheck,
- SystemCheck::App::OrphanedGroupMembersCheck,
- SystemCheck::App::GitlabConfigExistsCheck,
- SystemCheck::App::GitlabConfigUpToDateCheck,
- SystemCheck::App::LogWritableCheck,
- SystemCheck::App::TmpWritableCheck,
- SystemCheck::App::UploadsDirectoryExistsCheck,
- SystemCheck::App::UploadsPathPermissionCheck,
- SystemCheck::App::UploadsPathTmpPermissionCheck,
- SystemCheck::App::InitScriptExistsCheck,
- SystemCheck::App::InitScriptUpToDateCheck,
- SystemCheck::App::ProjectsHaveNamespaceCheck,
- SystemCheck::App::RedisVersionCheck,
- SystemCheck::App::RubyVersionCheck,
- SystemCheck::App::GitVersionCheck,
- SystemCheck::App::GitUserDefaultSSHConfigCheck,
- SystemCheck::App::ActiveUsersCheck
- ]
-
- SystemCheck.run('GitLab', checks)
+ SystemCheck::RakeTask::AppTask.run!
end
end
namespace :gitlab_shell do
desc "GitLab | Check the configuration of GitLab Shell"
task check: :gitlab_environment do
- warn_user_is_not_gitlab
- start_checking "GitLab Shell"
-
- check_gitlab_shell
- check_gitlab_shell_self_test
-
- finished_checking "GitLab Shell"
- end
-
- # Checks
- ########################
-
- def check_gitlab_shell_self_test
- gitlab_shell_repo_base = gitlab_shell_path
- check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base)
- puts "Running #{check_cmd}"
-
- if system(check_cmd, chdir: gitlab_shell_repo_base)
- puts 'gitlab-shell self-check successful'.color(:green)
- else
- puts 'gitlab-shell self-check failed'.color(:red)
- try_fixing_it(
- 'Make sure GitLab is running;',
- 'Check the gitlab-shell configuration file:',
- sudo_gitlab("editor #{File.expand_path('config.yml', gitlab_shell_repo_base)}")
- )
- fix_and_rerun
- end
- end
-
- # Helper methods
- ########################
-
- def gitlab_shell_path
- Gitlab.config.gitlab_shell.path
- end
-
- def gitlab_shell_version
- Gitlab::Shell.new.version
- end
-
- def gitlab_shell_major_version
- Gitlab::Shell.version_required.split('.')[0].to_i
- end
-
- def gitlab_shell_minor_version
- Gitlab::Shell.version_required.split('.')[1].to_i
- end
-
- def gitlab_shell_patch_version
- Gitlab::Shell.version_required.split('.')[2].to_i
+ SystemCheck::RakeTask::GitlabShellTask.run!
end
end
namespace :gitaly do
desc 'GitLab | Check the health of Gitaly'
task check: :gitlab_environment do
- warn_user_is_not_gitlab
- start_checking 'Gitaly'
-
- Gitlab::HealthChecks::GitalyCheck.readiness.each do |result|
- print "#{result.labels[:shard]} ... "
-
- if result.success
- puts 'OK'.color(:green)
- else
- puts "FAIL: #{result.message}".color(:red)
- end
- end
-
- finished_checking 'Gitaly'
+ SystemCheck::RakeTask::GitalyTask.run!
end
end
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
task check: :gitlab_environment do
- warn_user_is_not_gitlab
- start_checking "Sidekiq"
-
- check_sidekiq_running
- only_one_sidekiq_running
-
- finished_checking "Sidekiq"
- end
-
- # Checks
- ########################
-
- def check_sidekiq_running
- print "Running? ... "
-
- if sidekiq_process_count > 0
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- sudo_gitlab("RAILS_ENV=production bin/background_jobs start")
- )
- for_more_information(
- see_installation_guide_section("Install Init Script"),
- "see log/sidekiq.log for possible errors"
- )
- fix_and_rerun
- end
- end
-
- def only_one_sidekiq_running
- process_count = sidekiq_process_count
- return if process_count.zero?
-
- print 'Number of Sidekiq processes ... '
-
- if process_count == 1
- puts '1'.color(:green)
- else
- puts "#{process_count}".color(:red)
- try_fixing_it(
- 'sudo service gitlab stop',
- "sudo pkill -u #{gitlab_user} -f sidekiq",
- "sleep 10 && sudo pkill -9 -u #{gitlab_user} -f sidekiq",
- 'sudo service gitlab start'
- )
- fix_and_rerun
- end
- end
-
- def sidekiq_process_count
- ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
- ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
+ SystemCheck::RakeTask::SidekiqTask.run!
end
end
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :gitlab_environment do
- warn_user_is_not_gitlab
-
- if Gitlab.config.incoming_email.enabled
- checks = [
- SystemCheck::IncomingEmail::ImapAuthenticationCheck
- ]
-
- if Rails.env.production?
- checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
- checks << SystemCheck::IncomingEmail::MailRoomRunningCheck
- else
- checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck
- end
-
- SystemCheck.run('Reply by email', checks)
- else
- puts 'Reply by email is disabled in config/gitlab.yml'
- end
+ SystemCheck::RakeTask::IncomingEmailTask.run!
end
end
namespace :ldap do
task :check, [:limit] => :gitlab_environment do |_, args|
- # Only show up to 100 results because LDAP directories can be very big.
- # This setting only affects the `rake gitlab:check` script.
- args.with_defaults(limit: 100)
- warn_user_is_not_gitlab
- start_checking "LDAP"
-
- if Gitlab::Auth::LDAP::Config.enabled?
- check_ldap(args.limit)
- else
- puts 'LDAP is disabled in config/gitlab.yml'
- end
-
- finished_checking "LDAP"
- end
-
- def check_ldap(limit)
- servers = Gitlab::Auth::LDAP::Config.providers
-
- servers.each do |server|
- puts "Server: #{server}"
+ ENV['LDAP_CHECK_LIMIT'] = args.limit if args.limit.present?
- begin
- Gitlab::Auth::LDAP::Adapter.open(server) do |adapter|
- check_ldap_auth(adapter)
-
- puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
-
- users = adapter.users(adapter.config.uid, '*', limit)
- users.each do |user|
- puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
- end
- end
- rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
- puts "Could not connect to the LDAP server: #{e.message}".color(:red)
- end
- end
- end
-
- def check_ldap_auth(adapter)
- auth = adapter.config.has_auth?
-
- message = if auth && adapter.ldap.bind
- 'Success'.color(:green)
- elsif auth
- 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
- else
- 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
- end
-
- puts "LDAP authentication... #{message}"
+ SystemCheck::RakeTask::LdapTask.run!
end
end
namespace :orphans do
desc 'Gitlab | Check for orphaned namespaces and repositories'
task check: :gitlab_environment do
- warn_user_is_not_gitlab
- checks = [
- SystemCheck::Orphans::NamespaceCheck,
- SystemCheck::Orphans::RepositoryCheck
- ]
-
- SystemCheck.run('Orphans', checks)
+ SystemCheck::RakeTask::OrphansTask.run!
end
desc 'GitLab | Check for orphaned namespaces in the repositories path'
task check_namespaces: :gitlab_environment do
- warn_user_is_not_gitlab
- checks = [SystemCheck::Orphans::NamespaceCheck]
-
- SystemCheck.run('Orphans', checks)
+ SystemCheck::RakeTask::Orphans::NamespaceTask.run!
end
desc 'GitLab | Check for orphaned repositories in the repositories path'
task check_repositories: :gitlab_environment do
- warn_user_is_not_gitlab
- checks = [SystemCheck::Orphans::RepositoryCheck]
-
- SystemCheck.run('Orphans', checks)
- end
- end
-
- # Helper methods
- ##########################
-
- def check_gitlab_shell
- required_version = Gitlab::VersionInfo.new(gitlab_shell_major_version, gitlab_shell_minor_version, gitlab_shell_patch_version)
- current_version = Gitlab::VersionInfo.parse(gitlab_shell_version)
-
- print "GitLab Shell version >= #{required_version} ? ... "
- if current_version.valid? && required_version <= current_version
- puts "OK (#{current_version})".color(:green)
- else
- puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red)
+ SystemCheck::RakeTask::Orphans::RepositoryTask.run!
end
end
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index e8ae5dfa540..451ba651674 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
desc "GitLab | Cleanup | Clean namespaces"
task dirs: :gitlab_environment do
namespaces = Set.new(Namespace.pluck(:path))
- namespaces << Storage::HashedProject::ROOT_PATH_PREFIX
+ namespaces << Storage::HashedProject::REPOSITORY_PATH_PREFIX
Gitaly::Server.all.each do |server|
all_dirs = Gitlab::GitalyClient::StorageService
@@ -49,7 +49,7 @@ namespace :gitlab do
# TODO ignoring hashed repositories for now. But revisit to fully support
# possible orphaned hashed repos
- next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX)
+ next if repo_with_namespace.start_with?(Storage::HashedProject::REPOSITORY_PATH_PREFIX)
next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
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/lib/tasks/import.rake b/lib/tasks/import.rake
index a16d4c47273..f912f521dfb 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -42,7 +42,7 @@ class GithubImport
end
def import!
- @project.force_import_start
+ @project.import_state.force_start
import_success = false
@@ -57,7 +57,7 @@ class GithubImport
puts "Import finished. Timings: #{timings}".color(:green)
else
puts "Import was not successful. Errors were as follows:"
- puts @project.import_error
+ puts @project.import_state.last_error
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 837768efef2..d7d03151cdf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -141,6 +141,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] ""
@@ -158,6 +176,9 @@ msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
+msgid "%{user_name} profile page"
+msgstr ""
+
msgid "+ %{count} more"
msgstr ""
@@ -222,7 +243,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."
@@ -300,6 +321,9 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept invitation"
+msgstr ""
+
msgid "Accept terms"
msgstr ""
@@ -327,16 +351,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."
@@ -804,6 +828,9 @@ msgstr ""
msgid "Automatically marked as default internal user"
msgstr ""
+msgid "Automatically resolved"
+msgstr ""
+
msgid "Available"
msgstr ""
@@ -936,11 +963,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 ""
@@ -1088,9 +1110,15 @@ msgstr ""
msgid "Business metrics (Custom)"
msgstr ""
+msgid "By %{user_name}"
+msgstr ""
+
msgid "ByAuthor|by"
msgstr ""
+msgid "CHANGELOG"
+msgstr ""
+
msgid "CI / CD"
msgstr ""
@@ -1148,6 +1176,9 @@ msgstr ""
msgid "CICD|instance enabled"
msgstr ""
+msgid "CONTRIBUTING"
+msgstr ""
+
msgid "Callback URL"
msgstr ""
@@ -1190,9 +1221,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 ""
@@ -1232,6 +1260,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 ""
@@ -1373,9 +1404,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 ""
@@ -1559,15 +1599,15 @@ msgstr ""
msgid "ClusterIntegration|Knative"
msgstr ""
-msgid "ClusterIntegration|Knative (pronounced kay-nay-tiv) extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
-msgstr ""
-
msgid "ClusterIntegration|Knative Domain Name:"
msgstr ""
msgid "ClusterIntegration|Knative IP Address:"
msgstr ""
+msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
@@ -1796,6 +1836,9 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Command line instructions"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -1816,11 +1859,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 ""
@@ -2004,9 +2042,6 @@ msgstr ""
msgid "Contribution Charts"
msgstr ""
-msgid "Contribution guide"
-msgstr ""
-
msgid "Contributions for <strong>%{calendar_date}</strong>"
msgstr ""
@@ -2031,10 +2066,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"
@@ -2097,6 +2132,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 ""
@@ -2253,6 +2291,9 @@ msgstr ""
msgid "December"
msgstr ""
+msgid "Decline"
+msgstr ""
+
msgid "Decline and sign out"
msgstr ""
@@ -2579,6 +2620,9 @@ msgstr ""
msgid "Edit application"
msgstr ""
+msgid "Edit comment"
+msgstr ""
+
msgid "Edit environment"
msgstr ""
@@ -2831,6 +2875,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr ""
+msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
+msgstr ""
+
msgid "Every day (at 4:00am)"
msgstr ""
@@ -2846,6 +2893,12 @@ msgstr ""
msgid "Everyone can contribute"
msgstr ""
+msgid "Existing Git repository"
+msgstr ""
+
+msgid "Existing folder"
+msgstr ""
+
msgid "Expand"
msgstr ""
@@ -2918,6 +2971,9 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failed to upload object map file"
+msgstr ""
+
msgid "Failure"
msgstr ""
@@ -2951,9 +3007,6 @@ msgstr ""
msgid "Files"
msgstr ""
-msgid "Files (%{human_size})"
-msgstr ""
-
msgid "Filter"
msgstr ""
@@ -3080,6 +3133,9 @@ msgstr ""
msgid "Git"
msgstr ""
+msgid "Git global setup"
+msgstr ""
+
msgid "Git repository URL"
msgstr ""
@@ -3361,6 +3417,9 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation."
+msgstr ""
+
msgid "I accept the %{terms_link}"
msgstr ""
@@ -3415,6 +3474,9 @@ msgstr ""
msgid "Identities"
msgstr ""
+msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
+msgstr ""
+
msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
@@ -3436,6 +3498,9 @@ msgstr ""
msgid "ImageDiffViewer|Swipe"
msgstr ""
+msgid "Impersonation has been disabled"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -3487,6 +3552,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
+msgstr ""
+
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
msgstr ""
@@ -3538,6 +3606,9 @@ msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
+msgid "Internal"
+msgstr ""
+
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr ""
@@ -3553,6 +3624,9 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Invitation"
+msgstr ""
+
msgid "Invite"
msgstr ""
@@ -3640,7 +3714,7 @@ msgstr ""
msgid "Job|The artifacts will be removed in"
msgstr ""
-msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it."
+msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it."
msgstr ""
msgid "Jul"
@@ -3762,12 +3836,18 @@ msgstr ""
msgid "Latest changes"
msgstr ""
+msgid "Latest pipeline for this branch"
+msgstr ""
+
msgid "Learn more"
msgstr ""
msgid "Learn more about %{issue_boards_url}, to keep track of issues in multiple lists, using labels, assignees, and milestones. If you’re missing something from issue boards, please create an issue on %{gitlab_issues_url}."
msgstr ""
+msgid "Learn more about Auto DevOps"
+msgstr ""
+
msgid "Learn more about Kubernetes"
msgstr ""
@@ -3995,6 +4075,18 @@ msgstr ""
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
+msgid "MergeRequests|started a discussion"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}"
+msgstr ""
+
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
msgstr ""
@@ -4034,6 +4126,9 @@ msgstr ""
msgid "Metrics|Learn about environments"
msgstr ""
+msgid "Metrics|No data to display"
+msgstr ""
+
msgid "Metrics|No deployed environments"
msgstr ""
@@ -4249,9 +4344,6 @@ msgstr ""
msgid "No"
msgstr ""
-msgid "No Label"
-msgstr ""
-
msgid "No assignee"
msgstr ""
@@ -4279,6 +4371,9 @@ msgstr ""
msgid "No file chosen"
msgstr ""
+msgid "No file selected"
+msgstr ""
+
msgid "No files found."
msgstr ""
@@ -4345,6 +4440,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
+msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
+msgstr ""
+
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -4366,9 +4464,18 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Nothing here."
+msgstr ""
+
msgid "Notification events"
msgstr ""
+msgid "Notification setting"
+msgstr ""
+
+msgid "Notification setting - %{notification_title}"
+msgstr ""
+
msgid "NotificationEvent|Close issue"
msgstr ""
@@ -4563,9 +4670,6 @@ msgstr ""
msgid "People without permission will never get a notification and won't be able to comment."
msgstr ""
-msgid "Per job. If a job passes this threshold, it will be marked as failed"
-msgstr ""
-
msgid "Perform advanced options such as changing path, transferring, or removing the group."
msgstr ""
@@ -4827,6 +4931,9 @@ msgstr ""
msgid "Prioritized label"
msgstr ""
+msgid "Private"
+msgstr ""
+
msgid "Private - Project access must be granted explicitly to each user."
msgstr ""
@@ -5253,6 +5360,9 @@ msgstr ""
msgid "Provider"
msgstr ""
+msgid "Public"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -5277,6 +5387,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "README"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -5286,9 +5399,6 @@ msgstr ""
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr ""
-msgid "Readme"
-msgstr ""
-
msgid "Real-time features"
msgstr ""
@@ -5426,6 +5536,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 ""
@@ -5468,6 +5584,9 @@ msgstr ""
msgid "Resolve discussion"
msgstr ""
+msgid "Resolved"
+msgstr ""
+
msgid "Response metrics (AWS ELB)"
msgstr ""
@@ -5749,6 +5868,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 ""
@@ -6036,6 +6194,9 @@ msgstr ""
msgid "Source is not available"
msgstr ""
+msgid "Source project cannot be found."
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -6093,6 +6254,9 @@ msgstr ""
msgid "Start and due date"
msgstr ""
+msgid "Start cleanup"
+msgstr ""
+
msgid "Start date"
msgstr ""
@@ -6168,11 +6332,6 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
-msgid "Tag (%{tag_count})"
-msgid_plural "Tags (%{tag_count})"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Tags"
msgstr ""
@@ -6308,6 +6467,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 ""
@@ -6509,10 +6671,10 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
-msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:"
+msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:"
msgstr ""
-msgid "This job is stuck, because you don't have any active runners that can run this job."
+msgid "This job is stuck because you don't have any active runners that can run this job."
msgstr ""
msgid "This job is the most recent deployment to %{link}."
@@ -6542,6 +6704,9 @@ msgstr ""
msgid "This page will be removed in a future release."
msgstr ""
+msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
+msgstr ""
+
msgid "This project"
msgstr ""
@@ -6569,7 +6734,7 @@ msgstr ""
msgid "This source diff could not be displayed because it is too large."
msgstr ""
-msgid "This timeout will take precedence when lower than Project-defined timeout"
+msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds."
msgstr ""
msgid "This user has no identities"
@@ -6987,6 +7152,9 @@ msgstr ""
msgid "Upload file"
msgstr ""
+msgid "Upload object map"
+msgstr ""
+
msgid "UploadLink|click to upload"
msgstr ""
@@ -7577,6 +7745,11 @@ msgstr ""
msgid "disabled"
msgstr ""
+msgid "discussion resolved"
+msgid_plural "discussions resolved"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "done"
msgstr ""
@@ -7595,6 +7768,9 @@ msgstr ""
msgid "from"
msgstr ""
+msgid "group"
+msgstr ""
+
msgid "here"
msgstr ""
@@ -7841,6 +8017,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "project"
+msgstr ""
+
msgid "remaining"
msgstr ""
@@ -7852,6 +8031,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 587b7b50c30..ac4d5174610 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +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",
"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",
@@ -24,25 +25,25 @@
"@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.38.0",
- "@gitlab/ui": "^1.11.0",
+ "@gitlab/svgs": "^1.40.0",
+ "@gitlab/ui": "^1.14.0",
"apollo-boost": "^0.1.20",
"apollo-client": "^2.4.5",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-loader": "^8.0.4",
- "bootstrap": "4.1.1",
+ "bootstrap": "4.1.3",
"brace-expansion": "^1.1.8",
"cache-loader": "^1.2.2",
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
- "codesandbox-api": "^0.0.18",
+ "codesandbox-api": "^0.0.20",
"compression-webpack-plugin": "^2.0.0",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
"css-loader": "^1.0.0",
- "d3": "4.12.2",
+ "d3": "^4.13.0",
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
@@ -86,7 +87,7 @@
"sanitize-html": "^1.16.1",
"select2": "3.5.2-browserify",
"sha1": "^1.1.1",
- "smooshpack": "^0.0.48",
+ "smooshpack": "^0.0.53",
"sortablejs": "^1.7.0",
"sql.js": "^0.4.0",
"stickyfilljs": "^2.0.5",
@@ -118,17 +119,23 @@
"@gitlab/eslint-config": "^1.2.0",
"@vue/test-utils": "^1.0.0-beta.25",
"axios-mock-adapter": "^1.15.0",
+ "babel-core": "^7.0.0-bridge",
+ "babel-jest": "^23.6.0",
+ "babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-istanbul": "^5.1.0",
"babel-plugin-rewire": "^1.2.0",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-template": "^6.26.0",
"babel-types": "^6.26.0",
"chalk": "^2.4.1",
"commander": "^2.18.0",
"eslint": "~5.6.0",
+ "eslint-import-resolver-jest": "^2.1.1",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-plugin-html": "4.0.5",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jasmine": "^2.10.1",
+ "eslint-plugin-jest": "^22.1.0",
"gettext-extractor": "^3.3.2",
"gettext-extractor-vue": "^4.0.1",
"graphql-tag": "^2.10.0",
@@ -136,6 +143,8 @@
"jasmine-core": "^2.9.0",
"jasmine-diff": "^0.1.3",
"jasmine-jquery": "^2.1.1",
+ "jest": "^23.6.0",
+ "jest-junit": "^5.2.0",
"karma": "^3.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.4",
diff --git a/public/robots.txt b/public/robots.txt
index ea931e1a223..7908297564e 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -70,3 +70,5 @@ Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
+Disallow: /*/-/group_members
+Disallow: /*/project_members
diff --git a/qa/README.md b/qa/README.md
index 746bd5cf94b..08ba59e117d 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -80,6 +80,15 @@ GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sa
All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables).
+### Sending additional cookies
+
+The environment variable `QA_COOKIES` can be set to send additional cookies
+on every request. This is necessary on gitlab.com to direct traffic to the
+canary fleet. To do this set `QA_COOKIES="gitlab_canary=true"`.
+
+To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"`
+
+
### Building a Docker image to test
Once you have made changes to the CE/EE repositories, you may want to build a
diff --git a/qa/qa.rb b/qa/qa.rb
index aa0b78b37e8..10a50f5cc72 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -184,6 +184,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
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 91e229c4c8c..f4bba3c9560 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
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/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 9aaf57e8d83..7fd2ba25527 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -117,6 +117,15 @@ module QA
def perform(&block)
visit(url)
+ if QA::Runtime::Env.qa_cookies
+ browser = Capybara.current_session.driver.browser
+ QA::Runtime::Env.qa_cookies.each do |cookie|
+ name, value = cookie.split("=")
+ value ||= ""
+ browser.manage.add_cookie name: name, value: value
+ end
+ end
+
yield.tap { clear! } if block_given?
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 7b2768548dd..3bc2b44ccd8 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -38,6 +38,10 @@ module QA
ENV['CI'] || ENV['CI_SERVER']
end
+ def qa_cookies
+ ENV['QA_COOKIES'] && ENV['QA_COOKIES'].split(';')
+ end
+
def signup_disabled?
enabled?(ENV['SIGNUP_DISABLED'], default: false)
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index dae2a9e0236..8f24a27b26f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -1,7 +1,7 @@
module QA
context 'Manage', :smoke do
describe 'basic user login' do
- it 'user logs in using basic credentials' do
+ it 'user logs in using basic credentials and logs out' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -11,6 +11,15 @@ module QA
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
+
+ Page::Main::Menu.perform do |menu|
+ menu.sign_out
+ expect(menu).not_to have_personal_area
+ end
+
+ Page::Main::Login.perform do |form|
+ expect(form.sign_in_tab?).to be(true)
+ end
end
end
end
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/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/rubocop/cop/migration/add_reference.rb b/rubocop/cop/migration/add_reference.rb
index 4b67270c97a..1d471b9797e 100644
--- a/rubocop/cop/migration/add_reference.rb
+++ b/rubocop/cop/migration/add_reference.rb
@@ -8,7 +8,7 @@ module RuboCop
class AddReference < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_reference` requires `index: true`'
+ MSG = '`add_reference` requires `index: true` or `index: { options... }`'
def on_send(node)
return unless in_migration?(node)
@@ -33,7 +33,12 @@ module RuboCop
private
def index_enabled?(pair)
- hash_key_type(pair) == :sym && hash_key_name(pair) == :index && pair.children[1].true_type?
+ return unless hash_key_type(pair) == :sym
+ return unless hash_key_name(pair) == :index
+
+ index = pair.children[1]
+
+ index.true_type? || index.hash_type?
end
def hash_key_type(pair)
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 00e23f12bc0..b50bf2161cb 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -1,6 +1,8 @@
[[ "$TRACE" ]] && set -x
export TILLER_NAMESPACE="$KUBE_NAMESPACE"
+function echoerr() { printf "\033[0;31m%s\n\033[0m" "$*" >&2; }
+
function check_kube_domain() {
if [ -z ${REVIEW_APPS_DOMAIN+x} ]; then
echo "In order to deploy or use Review Apps, REVIEW_APPS_DOMAIN variable must be set"
@@ -88,19 +90,16 @@ function deploy() {
replicas="1"
service_enabled="false"
postgres_enabled="$POSTGRES_ENABLED"
- gitlab_migrations_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-rails-ce"
- gitlab_sidekiq_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-sidekiq-ce"
- gitlab_unicorn_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-unicorn-ce"
- gitlab_gitaly_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly"
- gitlab_shell_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell"
- gitlab_workhorse_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-workhorse-ce"
-
- if [[ "$CI_PROJECT_NAME" == "gitlab-ee" ]]; then
- gitlab_migrations_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-rails-ee"
- gitlab_sidekiq_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-sidekiq-ee"
- gitlab_unicorn_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-unicorn-ee"
- gitlab_workhorse_image_repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-workhorse-ee"
- fi
+
+ IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
+ IMAGE_VERSION="${CI_PROJECT_NAME#gitlab-}"
+ gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-${IMAGE_VERSION}"
+ gitlab_sidekiq_image_repository="${IMAGE_REPOSITORY}/gitlab-sidekiq-${IMAGE_VERSION}"
+ gitlab_unicorn_image_repository="${IMAGE_REPOSITORY}/gitlab-unicorn-${IMAGE_VERSION}"
+ gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-${IMAGE_VERSION}"
+ gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly"
+ gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell"
+ gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${IMAGE_VERSION}"
# canary uses stable db
[[ "$track" == "canary" ]] && postgres_enabled="false"
@@ -155,6 +154,8 @@ HELM_CMD=$(cat << EOF
--set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_NAME" \
--set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \
--set gitlab.unicorn.image.tag="$CI_COMMIT_REF_NAME" \
+ --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \
+ --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_NAME" \
--set gitlab.gitaly.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly" \
--set gitlab.gitaly.image.tag="v$GITALY_VERSION" \
--set gitlab.gitlab-shell.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell" \
@@ -229,3 +230,122 @@ function install_external_dns() {
--set rbac.create="true"
fi
}
+
+function get_pod() {
+ local app_name="${1}"
+ local status="${2-Running}"
+ get_pod_cmd="kubectl get pods -n ${KUBE_NAMESPACE} --field-selector=status.phase=${status} -lapp=${app_name},release=${CI_ENVIRONMENT_SLUG} --no-headers -o=custom-columns=NAME:.metadata.name"
+ echoerr "Running '${get_pod_cmd}'"
+
+ while true; do
+ local pod_name="$(eval $get_pod_cmd)"
+ [[ "${pod_name}" == "" ]] || break
+
+ echoerr "Waiting till '${app_name}' pod is ready";
+ sleep 5;
+ done
+
+ echoerr "The pod name is '${pod_name}'."
+ echo "${pod_name}"
+}
+
+function add_license() {
+ if [ -z "${REVIEW_APPS_EE_LICENSE}" ]; then echo "License not found" && return; fi
+
+ task_runner_pod=$(get_pod "task-runner");
+ if [ -z "${task_runner_pod}" ]; then echo "Task runner pod not found" && return; fi
+
+ echo "${REVIEW_APPS_EE_LICENSE}" > /tmp/license.gitlab
+ kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab ${task_runner_pod}:/tmp/license.gitlab
+ rm /tmp/license.gitlab
+
+ kubectl -n "$KUBE_NAMESPACE" exec -it ${task_runner_pod} -- /srv/gitlab/bin/rails runner -e production \
+ '
+ content = File.read("/tmp/license.gitlab").strip;
+ FileUtils.rm_f("/tmp/license.gitlab");
+
+ unless License.where(data:content).empty?
+ puts "License already exists";
+ Kernel.exit 0;
+ end
+
+ unless License.new(data: content).save
+ puts "Could not add license";
+ Kernel.exit 0;
+ end
+
+ puts "License added";
+ '
+}
+
+function get_job_id() {
+ local job_name="${1}"
+ local query_string="${2:+&${2}}"
+
+ local max_page=3
+ local page=1
+
+ while true; do
+ 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 "map(select(.name == \"${job_name}\")) | map(.id) | last")
+ [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
+
+ ((page++))
+ done
+
+ if [[ "${job_id}" == "" ]]; then
+ echoerr "The '${job_name}' job ID couldn't be retrieved!"
+ else
+ echoerr "The '${job_name}' job ID is ${job_id}"
+ echo "${job_id}"
+ fi
+}
+
+function play_job() {
+ local job_name="${1}"
+ local job_id=$(get_job_id "${job_name}" "scope=manual");
+ if [ -z "${job_id}" ]; then return; fi
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
+ echoerr "POST ${url}"
+
+ local job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url")
+ echo "Manual job '${job_name}' started at: ${job_url}"
+}
+
+function wait_for_job_to_be_done() {
+ local job_name="${1}"
+ local query_string="${2}"
+ local job_id=$(get_job_id "${job_name}" "${query_string}");
+ if [ -z "${job_id}" ]; then return; fi
+
+ echoerr "Waiting for the '${job_name}' job to finish..."
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}"
+ echo "GET ${url}"
+
+ # In case the job hasn't finished yet. Keep trying until the job times out.
+ local interval=30
+ 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_seconds+=$interval))
+ sleep ${interval}
+ done
+
+ local elapsed_minutes=$((elapsed_seconds / 60))
+ echoerr "Waited '${job_name}' for ${elapsed_minutes} minutes."
+
+ if [[ "${job_status}" == "failed" ]]; then
+ echo "The '${job_name}' failed."
+ elif [[ "${job_status}" == "manual" ]]; then
+ echo "The '${job_name}' is manual."
+ else
+ echo "The '${job_name}' passed."
+ fi
+}
diff --git a/scripts/trigger-build b/scripts/trigger-build
index d2c71f5be3e..14af3281106 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -68,6 +68,7 @@ module Trigger
def base_variables
{
+ 'GITLAB_REF_SLUG' => ref_slug,
'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'],
'TRIGGER_SOURCE' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
@@ -76,6 +77,12 @@ module Trigger
}
end
+ def ref_slug
+ return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)}
+
+ ENV['CI_COMMIT_REF_SLUG']
+ end
+
# Read version files from all components
def version_file_variables
Dir.glob("*_VERSION").each_with_object({}) do |version_file, params|
@@ -106,18 +113,11 @@ module Trigger
def extra_variables
{
'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'],
- 'GITLAB_REF_SLUG' => ref_slug,
'ALTERNATIVE_SOURCES' => 'true',
'ee' => Trigger.ee? ? 'true' : 'false',
'QA_BRANCH' => ENV['QA_BRANCH'] || 'master'
}
end
-
- def ref_slug
- return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)}
-
- ENV['CI_COMMIT_REF_SLUG']
- end
end
class CNG < Base
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 83b2de47741..c89b5f48dc0 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -6,4 +6,102 @@ describe Settings do
expect(described_class.omniauth.enabled).to be true
end
end
+
+ describe '.attr_encrypted_db_key_base_truncated' do
+ it 'is a string with maximum 32 bytes size' do
+ expect(described_class.attr_encrypted_db_key_base_truncated.bytesize)
+ .to be <= 32
+ end
+ end
+
+ describe '.attr_encrypted_db_key_base_12' do
+ context 'when db key base secret is less than 12 bytes' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('a' * 10)
+ end
+
+ it 'expands db key base secret to 12 bytes' do
+ expect(described_class.attr_encrypted_db_key_base_12)
+ .to eq(('a' * 10) + ('0' * 2))
+ end
+ end
+
+ context 'when key has multiple multi-byte UTF chars exceeding 12 bytes' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('❤' * 18)
+ end
+
+ it 'does not use more than 32 bytes' do
+ db_key_base = described_class.attr_encrypted_db_key_base_12
+
+ expect(db_key_base).to eq('❤' * 4)
+ expect(db_key_base.bytesize).to eq 12
+ end
+ end
+ end
+
+ describe '.attr_encrypted_db_key_base_32' do
+ context 'when db key base secret is less than 32 bytes' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('a' * 10)
+ end
+
+ it 'expands db key base secret to 32 bytes' do
+ expanded_key_base = ('a' * 10) + ('0' * 22)
+
+ expect(expanded_key_base.bytesize).to eq 32
+ expect(described_class.attr_encrypted_db_key_base_32)
+ .to eq expanded_key_base
+ end
+ end
+
+ context 'when db key base secret is 32 bytes' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('a' * 32)
+ end
+
+ it 'returns original value' do
+ expect(described_class.attr_encrypted_db_key_base_32)
+ .to eq 'a' * 32
+ end
+ end
+
+ context 'when db key base contains multi-byte UTF character' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('❤' * 6)
+ end
+
+ it 'does not use more than 32 bytes' do
+ db_key_base = described_class.attr_encrypted_db_key_base_32
+
+ expect(db_key_base).to eq '❤❤❤❤❤❤' + ('0' * 14)
+ expect(db_key_base.bytesize).to eq 32
+ end
+ end
+
+ context 'when db key base multi-byte UTF chars exceeding 32 bytes' do
+ before do
+ allow(described_class)
+ .to receive(:attr_encrypted_db_key_base)
+ .and_return('❤' * 18)
+ end
+
+ it 'does not use more than 32 bytes' do
+ db_key_base = described_class.attr_encrypted_db_key_base_32
+
+ expect(db_key_base).to eq(('❤' * 10) + ('0' * 2))
+ expect(db_key_base.bytesize).to eq 32
+ end
+ end
+ end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index f350641a643..3dd0b2623ac 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -264,5 +264,17 @@ describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end
end
+
+ context "when impersonation is disabled" do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ end
+
+ it "shows error page" do
+ post :impersonate, id: user.username
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index efc3ce74627..c2bd7fd9808 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -107,59 +107,6 @@ describe ApplicationController do
end
end
- describe "#authenticate_user_from_personal_access_token!" do
- before do
- stub_authentication_activity_metrics(debug: false)
- end
-
- controller(described_class) do
- def index
- render text: 'authenticated'
- end
- end
-
- let(:personal_access_token) { create(:personal_access_token, user: user) }
-
- context "when the 'personal_access_token' param is populated with the personal access token" do
- it "logs the user in" do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_session_override_counter)
- .and increment(:user_sessionless_authentication_counter)
-
- get :index, private_token: personal_access_token.token
-
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
- end
-
- context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
- it "logs the user in" do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_session_override_counter)
- .and increment(:user_sessionless_authentication_counter)
-
- @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
- get :index
-
- expect(response).to have_gitlab_http_status(200)
- expect(response.body).to eq('authenticated')
- end
- end
-
- it "doesn't log the user in otherwise" do
- expect(authentication_metrics)
- .to increment(:user_unauthenticated_counter)
-
- get :index, private_token: "token"
-
- expect(response.status).not_to eq(200)
- expect(response.body).not_to eq('authenticated')
- end
- end
-
describe 'session expiration' do
controller(described_class) do
# The anonymous controller will report 401 and fail to run any actions.
@@ -167,7 +114,7 @@ describe ApplicationController do
skip_before_action :authenticate_user!, only: :index
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
end
@@ -224,74 +171,6 @@ describe ApplicationController do
end
end
- describe '#authenticate_sessionless_user!' do
- before do
- stub_authentication_activity_metrics(debug: false)
- end
-
- describe 'authenticating a user from a feed token' do
- controller(described_class) do
- def index
- render text: 'authenticated'
- end
- end
-
- context "when the 'feed_token' param is populated with the feed token" do
- context 'when the request format is atom' do
- it "logs the user in" do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_session_override_counter)
- .and increment(:user_sessionless_authentication_counter)
-
- get :index, feed_token: user.feed_token, format: :atom
-
- expect(response).to have_gitlab_http_status 200
- expect(response.body).to eq 'authenticated'
- end
- end
-
- context 'when the request format is ics' do
- it "logs the user in" do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_session_override_counter)
- .and increment(:user_sessionless_authentication_counter)
-
- get :index, feed_token: user.feed_token, format: :ics
-
- expect(response).to have_gitlab_http_status 200
- expect(response.body).to eq 'authenticated'
- end
- end
-
- context 'when the request format is neither atom nor ics' do
- it "doesn't log the user in" do
- expect(authentication_metrics)
- .to increment(:user_unauthenticated_counter)
-
- get :index, feed_token: user.feed_token
-
- expect(response.status).not_to have_gitlab_http_status 200
- expect(response.body).not_to eq 'authenticated'
- end
- end
- end
-
- context "when the 'feed_token' param is populated with an invalid feed token" do
- it "doesn't log the user" do
- expect(authentication_metrics)
- .to increment(:user_unauthenticated_counter)
-
- get :index, feed_token: 'token', format: :atom
-
- expect(response.status).not_to eq 200
- expect(response.body).not_to eq 'authenticated'
- end
- end
- end
- end
-
describe '#route_not_found' do
it 'renders 404 if authenticated' do
allow(controller).to receive(:current_user).and_return(user)
@@ -522,7 +401,7 @@ describe ApplicationController do
context 'terms' do
controller(described_class) do
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
end
@@ -557,36 +436,6 @@ describe ApplicationController do
expect(response).to have_gitlab_http_status(200)
end
-
- context 'for sessionless users' do
- render_views
-
- before do
- sign_out user
- end
-
- it 'renders a 403 when the sessionless user did not accept the terms' do
- get :index, feed_token: user.feed_token, format: :atom
-
- expect(response).to have_gitlab_http_status(403)
- end
-
- it 'renders the error message when the format was html' do
- get :index,
- private_token: create(:personal_access_token, user: user).token,
- format: :html
-
- expect(response.body).to have_content /accept the terms of service/i
- end
-
- it 'renders a 200 when the sessionless user accepted the terms' do
- accept_terms(user)
-
- get :index, feed_token: user.feed_token, format: :atom
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
end
end
@@ -595,7 +444,7 @@ describe ApplicationController do
attr_reader :last_payload
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
def append_info_to_payload(payload)
@@ -611,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/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 98946e4287b..6d0483f0032 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -50,7 +50,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body)
- expect(response).to match_response_schema('issues')
+ expect(response).to match_response_schema('entities/issue_boards')
expect(parsed_response['issues'].length).to eq 2
expect(development.issues.map(&:relative_position)).not_to include(nil)
end
@@ -121,7 +121,7 @@ describe Boards::IssuesController do
parsed_response = JSON.parse(response.body)
- expect(response).to match_response_schema('issues')
+ expect(response).to match_response_schema('entities/issue_boards')
expect(parsed_response['issues'].length).to eq 2
end
end
@@ -168,7 +168,7 @@ describe Boards::IssuesController do
it 'returns the created issue' do
create_issue user: user, board: board, list: list1, title: 'New issue'
- expect(response).to match_response_schema('issue')
+ expect(response).to match_response_schema('entities/issue_board')
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index e93c923fd39..f87eed6ff9f 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -86,6 +86,7 @@ describe IssuableCollections do
it 'only allows whitelisted params' do
allow(controller).to receive(:cookies).and_return({})
+ allow(controller).to receive(:current_user).and_return(nil)
finder_options = controller.send(:finder_options)
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
new file mode 100644
index 00000000000..2975205e09c
--- /dev/null
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Dashboard::ProjectsController do
+ it_behaves_like 'authenticates sessionless user', :index, :atom
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index b4a731fd3a3..e2c799f5205 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -42,6 +42,16 @@ describe Dashboard::TodosController do
end
end
+ context 'group authorization' do
+ it 'renders 404 when user does not have read access on given group' do
+ unauthorized_group = create(:group, :private)
+
+ get :index, group_id: unauthorized_group.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
context 'when using pagination' do
let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 187542ba30c..c857a78d5e8 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -1,21 +1,26 @@
require 'spec_helper'
describe DashboardController do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ context 'signed in' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
- describe 'GET issues' do
- it_behaves_like 'issuables list meta-data', :issue, :issues
- it_behaves_like 'issuables requiring filter', :issues
- end
+ describe 'GET issues' do
+ it_behaves_like 'issuables list meta-data', :issue, :issues
+ it_behaves_like 'issuables requiring filter', :issues
+ end
- describe 'GET merge requests' do
- it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
- it_behaves_like 'issuables requiring filter', :merge_requests
+ describe 'GET merge requests' do
+ it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
+ it_behaves_like 'issuables requiring filter', :merge_requests
+ end
end
+
+ it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
+ it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 1449036e148..949ad532365 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -52,15 +52,58 @@ describe GraphqlController do
end
end
+ context 'token authentication' do
+ before do
+ stub_authentication_activity_metrics(debug: false)
+ end
+
+ let(:user) { create(:user, username: 'Simon') }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it 'logs the user in' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_session_override_counter)
+ .and increment(:user_sessionless_authentication_counter)
+
+ run_test_query!(private_token: personal_access_token.token)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(query_response).to eq('echo' => '"Simon" says: test success')
+ end
+ end
+
+ context 'when the personal access token has no api scope' do
+ it 'does not log the user in' do
+ personal_access_token.update(scopes: [:read_user])
+
+ run_test_query!(private_token: personal_access_token.token)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(query_response).to eq('echo' => 'nil says: test success')
+ end
+ end
+
+ context 'without token' do
+ it 'shows public data' do
+ run_test_query!
+
+ expect(query_response).to eq('echo' => 'nil says: test success')
+ end
+ end
+ end
+
# Chosen to exercise all the moving parts in GraphqlController#execute
- def run_test_query!(variables: { 'text' => 'test success' })
+ def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil)
query = <<~QUERY
query Echo($text: String) {
echo(text: $text)
}
QUERY
- post :execute, query: query, operationName: 'Echo', variables: variables
+ post :execute, query: query, operationName: 'Echo', variables: variables, private_token: private_token
end
def query_response
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 4de61b65f71..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
@@ -606,4 +607,24 @@ describe GroupsController do
end
end
end
+
+ context 'token authentication' do
+ it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
+ before do
+ default_params.merge!(id: group)
+ end
+ end
+
+ it_behaves_like 'authenticates sessionless user', :issues, :atom, public: true do
+ before do
+ default_params.merge!(id: group, author_id: user.id)
+ end
+ end
+
+ it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics, public: true do
+ before do
+ default_params.merge!(id: group)
+ end
+ end
+ end
end
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index 77060fdc3be..db912641894 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -126,7 +126,7 @@ describe Import::BitbucketServerController do
end
it 'assigns repository categories' do
- created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_status: 'finished', import_source: @created_repo.browse_url)
+ created_project = create(:project, :import_finished, import_type: 'bitbucket_server', creator_id: user.id, import_source: @created_repo.browse_url)
repos = instance_double(BitbucketServer::Collection)
expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index ace8a954e92..b4219856fc0 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -40,6 +40,23 @@ describe Oauth::ApplicationsController do
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(profile_path)
end
+
+ context 'redirect_uri' do
+ render_views
+
+ it 'shows an error for a forbidden URI' do
+ invalid_uri_params = {
+ doorkeeper_application: {
+ name: 'foo',
+ redirect_uri: 'javascript://alert()'
+ }
+ }
+
+ post :create, invalid_uri_params
+
+ expect(response.body).to include 'Redirect URI is forbidden by the server'
+ end
+ end
end
end
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/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 3c5a21c47fa..9fc6af6a045 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -35,6 +35,11 @@ describe Projects::BlobController do
let(:id) { 'binary-encoding/encoding/binary-1.bin' }
it { is_expected.to respond_with(:success) }
end
+
+ context "Markdown file" do
+ let(:id) { 'master/README.md' }
+ it { is_expected.to respond_with(:success) }
+ end
end
context 'with file path and JSON format' do
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index a43bdd3ea80..5c72dab698c 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -5,87 +5,115 @@ describe Projects::CommitsController do
let(:user) { create(:user) }
before do
- sign_in(user)
project.add_maintainer(user)
end
- describe "GET commits_root" do
- context "no ref is provided" do
- it 'should redirect to the default branch of the project' do
- get(:commits_root,
- namespace_id: project.namespace,
- project_id: project)
+ context 'signed in' do
+ before do
+ sign_in(user)
+ end
+
+ describe "GET commits_root" do
+ context "no ref is provided" do
+ it 'should redirect to the default branch of the project' do
+ get(:commits_root,
+ namespace_id: project.namespace,
+ project_id: project)
- expect(response).to redirect_to project_commits_path(project)
+ expect(response).to redirect_to project_commits_path(project)
+ end
end
end
- end
- describe "GET show" do
- render_views
+ describe "GET show" do
+ render_views
- context 'with file path' do
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: id)
- end
+ context 'with file path' do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id)
+ end
- context "valid branch, valid file" do
- let(:id) { 'master/README.md' }
+ context "valid branch, valid file" do
+ let(:id) { 'master/README.md' }
- it { is_expected.to respond_with(:success) }
- end
+ it { is_expected.to respond_with(:success) }
+ end
- context "valid branch, invalid file" do
- let(:id) { 'master/invalid-path.rb' }
+ context "valid branch, invalid file" do
+ let(:id) { 'master/invalid-path.rb' }
- it { is_expected.to respond_with(:not_found) }
- end
+ it { is_expected.to respond_with(:not_found) }
+ end
- context "invalid branch, valid file" do
- let(:id) { 'invalid-branch/README.md' }
+ context "invalid branch, valid file" do
+ let(:id) { 'invalid-branch/README.md' }
- it { is_expected.to respond_with(:not_found) }
+ it { is_expected.to respond_with(:not_found) }
+ end
end
- end
- context "when the ref name ends in .atom" do
- context "when the ref does not exist with the suffix" do
- before do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: "master.atom")
+ context "when the ref name ends in .atom" do
+ context "when the ref does not exist with the suffix" do
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: "master.atom")
+ end
+
+ it "renders as atom" do
+ expect(response).to be_success
+ expect(response.content_type).to eq('application/atom+xml')
+ end
+
+ it 'renders summary with type=html' do
+ expect(response.body).to include('<summary type="html">')
+ end
end
- it "renders as atom" do
- expect(response).to be_success
- expect(response.content_type).to eq('application/atom+xml')
- end
+ context "when the ref exists with the suffix" do
+ before do
+ commit = project.repository.commit('master')
- it 'renders summary with type=html' do
- expect(response.body).to include('<summary type="html">')
+ allow_any_instance_of(Repository).to receive(:commit).and_call_original
+ allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
+
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: "master.atom")
+ end
+
+ it "renders as HTML" do
+ expect(response).to be_success
+ expect(response.content_type).to eq('text/html')
+ end
end
end
+ end
+ end
- context "when the ref exists with the suffix" do
+ context 'token authentication' do
+ context 'public project' do
+ it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
- commit = project.repository.commit('master')
+ public_project = create(:project, :repository, :public)
- allow_any_instance_of(Repository).to receive(:commit).and_call_original
- allow_any_instance_of(Repository).to receive(:commit).with('master.atom').and_return(commit)
-
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: "master.atom")
+ default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom")
end
+ end
+ end
+
+ context 'private project' do
+ it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do
+ before do
+ private_project = create(:project, :repository, :private)
+ private_project.add_maintainer(user)
- it "renders as HTML" do
- expect(response).to be_success
- expect(response.content_type).to eq('text/html')
+ default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom")
end
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index bc17331f531..5fa0488014f 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -217,7 +217,10 @@ describe Projects::EnvironmentsController do
end
it 'loads the terminals for the environment' do
- expect_any_instance_of(Environment).to receive(:terminals)
+ # In EE we have to stub EE::Environment since it overwrites the
+ # "terminals" method.
+ expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ .to receive(:terminals)
get :terminal, environment_params
end
@@ -240,7 +243,9 @@ describe Projects::EnvironmentsController do
context 'and valid id' do
it 'returns the first terminal for the environment' do
- expect_any_instance_of(Environment)
+ # In EE we have to stub EE::Environment since it overwrites the
+ # "terminals" method.
+ expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
.to receive(:terminals)
.and_return([:fake_terminal])
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index adf3c78ae51..cdc63f5aab3 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -26,10 +26,11 @@ describe Projects::ImportsController do
context 'when repository exists' do
let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') }
+ let(:import_state) { project.import_state }
context 'when import is in progress' do
before do
- project.update(import_status: :started)
+ import_state.update(status: :started)
end
it 'renders template' do
@@ -47,7 +48,7 @@ describe Projects::ImportsController do
context 'when import failed' do
before do
- project.update(import_status: :failed)
+ import_state.update(status: :failed)
end
it 'redirects to new_namespace_project_import_path' do
@@ -59,7 +60,7 @@ describe Projects::ImportsController do
context 'when import finished' do
before do
- project.update(import_status: :finished)
+ import_state.update(status: :finished)
end
context 'when project is a fork' do
@@ -108,7 +109,7 @@ describe Projects::ImportsController do
context 'when import never happened' do
before do
- project.update(import_status: :none)
+ import_state.update(status: :none)
end
it 'redirects to namespace_project_path' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 80138183c07..02930edbf72 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1068,4 +1068,40 @@ describe Projects::IssuesController do
end
end
end
+
+ context 'private project with token authentication' do
+ let(:private_project) { create(:project, :private) }
+
+ it_behaves_like 'authenticates sessionless user', :index, :atom do
+ before do
+ default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
+
+ private_project.add_maintainer(user)
+ end
+ end
+
+ it_behaves_like 'authenticates sessionless user', :calendar, :ics do
+ before do
+ default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
+
+ private_project.add_maintainer(user)
+ end
+ end
+ end
+
+ context 'public project with token authentication' do
+ let(:public_project) { create(:project, :public) }
+
+ it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
+ before do
+ default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
+ end
+ end
+
+ it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do
+ before do
+ default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index bd193979192..fca313dafb1 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -876,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
@@ -904,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/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 9dc06436c72..8fc5d302af6 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -36,6 +36,18 @@ describe Projects::MergeRequests::DiffsController do
end
end
+ context 'when note has no position' do
+ before do
+ create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request, position: nil)
+ end
+
+ it 'serializes merge request diff collection' do
+ expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+
+ go
+ end
+ end
+
context 'with forked projects with submodules' do
render_views
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index e62523c65c9..7f15da859e5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -290,6 +290,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/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index ccd4fc4db3a..658aa2a6738 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -143,11 +143,27 @@ describe Projects::MilestonesController do
end
describe '#promote' do
+ let(:group) { create(:group) }
+
+ before do
+ project.update(namespace: group)
+ end
+
+ context 'when user does not have permission to promote milestone' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'renders 404' do
+ post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
context 'promotion succeeds' do
before do
- group = create(:group)
group.add_developer(user)
- milestone.project.update(namespace: group)
end
it 'shows group milestone' do
@@ -166,12 +182,17 @@ describe Projects::MilestonesController do
end
end
- context 'promotion fails' do
- it 'shows project milestone' do
+ context 'when user cannot admin group milestones' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'renders 404' do
+ project.update(namespace: user.namespace)
+
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
- expect(response).to redirect_to(project_milestone_path(project, milestone))
- expect(flash[:alert]).to eq('Promotion failed - Project does not belong to a group.')
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 9ac7b8ee8a8..d2a26068362 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -283,14 +283,14 @@ describe Projects::NotesController do
def post_create(extra_params = {})
post :create, {
- note: { note: 'some other note' },
- namespace_id: project.namespace,
- project_id: project,
- target_type: 'merge_request',
- target_id: merge_request.id,
- note_project_id: forked_project.id,
- in_reply_to_discussion_id: existing_comment.discussion_id
- }.merge(extra_params)
+ note: { note: 'some other note', noteable_id: merge_request.id },
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'merge_request',
+ target_id: merge_request.id,
+ note_project_id: forked_project.id,
+ in_reply_to_discussion_id: existing_comment.discussion_id
+ }.merge(extra_params)
end
context 'when the note_project_id is not correct' do
@@ -324,6 +324,30 @@ describe Projects::NotesController do
end
end
+ context 'when target_id and noteable_id do not match' do
+ let(:locked_issue) { create(:issue, :locked, project: project) }
+ let(:issue) {create(:issue, project: project)}
+
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ project.project_member(user).destroy
+ end
+
+ it 'uses target_id and ignores noteable_id' do
+ request_params = {
+ note: { note: 'some note', noteable_type: 'Issue', noteable_id: locked_issue.id },
+ target_type: 'issue',
+ target_id: issue.id,
+ project_id: project,
+ namespace_id: project.namespace
+ }
+
+ expect { post :create, request_params }.to change { issue.notes.count }.by(1)
+ .and change { locked_issue.notes.count }.by(0)
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
context 'when the merge request discussion is locked' do
before do
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
@@ -376,35 +400,60 @@ describe Projects::NotesController do
end
describe 'PUT update' do
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- id: note,
- format: :json,
- note: {
- note: "New comment"
+ context "should update the note with a valid issue" do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :json,
+ note: {
+ note: "New comment"
+ }
}
- }
- end
+ end
- before do
- sign_in(note.author)
- project.add_developer(note.author)
+ before do
+ sign_in(note.author)
+ project.add_developer(note.author)
+ end
+
+ it "updates the note" do
+ expect { put :update, request_params }.to change { note.reload.note }
+ end
end
+ context "doesnt update the note" do
+ let(:issue) { create(:issue, :confidential, project: project) }
+ let(:note) { create(:note, noteable: issue, project: project) }
- it "updates the note" do
- expect { put :update, request_params }.to change { note.reload.note }
+ before do
+ sign_in(user)
+ project.add_guest(user)
+ end
+
+ it "disallows edits when the issue is confidential and the user has guest permissions" do
+ request_params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :json,
+ note: {
+ note: "New comment"
+ }
+ }
+ expect { put :update, request_params }.not_to change { note.reload.note }
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
describe 'DELETE destroy' do
let(:request_params) do
{
- namespace_id: project.namespace,
- project_id: project,
- id: note,
- format: :js
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :js
}
end
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/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index c48f41ca12e..6fbf75d0259 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -35,4 +35,26 @@ describe Projects::TagsController do
it { is_expected.to respond_with(:not_found) }
end
end
+
+ context 'private project with token authentication' do
+ let(:private_project) { create(:project, :repository, :private) }
+
+ it_behaves_like 'authenticates sessionless user', :index, :atom do
+ before do
+ default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
+
+ private_project.add_maintainer(user)
+ end
+ end
+ end
+
+ context 'public project with token authentication' do
+ let(:public_project) { create(:project, :repository, :public) }
+
+ it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
+ before do
+ default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
+ 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 3bc9cbe64c5..7849bec4762 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -882,6 +882,28 @@ describe ProjectsController do
end
end
+ context 'private project with token authentication' do
+ let(:private_project) { create(:project, :private) }
+
+ it_behaves_like 'authenticates sessionless user', :show, :atom do
+ before do
+ default_params.merge!(id: private_project, namespace_id: private_project.namespace)
+
+ private_project.add_maintainer(user)
+ end
+ end
+ end
+
+ context 'public project with token authentication' do
+ let(:public_project) { create(:project, :public) }
+
+ it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
+ before do
+ default_params.merge!(id: public_project, namespace_id: public_project.namespace)
+ end
+ end
+ end
+
def project_moved_message(redirect_route, project)
"Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
end
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/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 071f96a729e..fe438e71e9e 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -395,6 +395,14 @@ describe UsersController do
end
end
+ context 'token authentication' do
+ it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
+ before do
+ default_params.merge!(username: user.username)
+ end
+ end
+ end
+
def user_moved_message(redirect_route, user)
"User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path."
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 90754319f05..07c1fc31152 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -308,7 +308,7 @@ FactoryBot.define do
trait :with_runner_session do
after(:build) do |build|
- build.build_runner_session(url: 'ws://localhost')
+ build.build_runner_session(url: 'https://localhost')
end
end
end
diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb
index 6ad93fb0f45..3b50a57433f 100644
--- a/spec/factories/clusters/kubernetes_namespaces.rb
+++ b/spec/factories/clusters/kubernetes_namespaces.rb
@@ -5,10 +5,12 @@ FactoryBot.define do
association :cluster, :project, :provided_by_gcp
after(:build) do |kubernetes_namespace|
- cluster_project = kubernetes_namespace.cluster.cluster_project
+ if kubernetes_namespace.cluster.project_type?
+ cluster_project = kubernetes_namespace.cluster.cluster_project
- kubernetes_namespace.project = cluster_project.project
- kubernetes_namespace.cluster_project = cluster_project
+ kubernetes_namespace.project = cluster_project.project
+ kubernetes_namespace.cluster_project = cluster_project
+ end
end
trait :with_token do
diff --git a/spec/factories/import_state.rb b/spec/factories/import_state.rb
index 15d0a9d466a..d6de26dccbc 100644
--- a/spec/factories/import_state.rb
+++ b/spec/factories/import_state.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
transient do
import_url { generate(:url) }
+ import_type nil
end
trait :repository do
@@ -32,7 +33,11 @@ FactoryBot.define do
end
after(:create) do |import_state, evaluator|
- import_state.project.update_columns(import_url: evaluator.import_url)
+ columns = {}
+ columns[:import_url] = evaluator.import_url unless evaluator.import_url.blank?
+ columns[:import_type] = evaluator.import_type unless evaluator.import_type.blank?
+
+ import_state.project.update_columns(columns)
end
end
end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
new file mode 100644
index 00000000000..2ed0844ed47
--- /dev/null
+++ b/spec/factories/pool_repositories.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :pool_repository do
+ shard
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index e4823a5adf1..1906c06a211 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -30,6 +30,8 @@ FactoryBot.define do
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
group_runners_enabled nil
+ import_status nil
+ import_jid nil
end
after(:create) do |project, evaluator|
@@ -64,6 +66,13 @@ FactoryBot.define do
# assign the delegated `#ci_cd_settings` attributes after create
project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
+
+ if evaluator.import_status
+ import_state = project.import_state || project.build_import_state
+ import_state.status = evaluator.import_status
+ import_state.jid = evaluator.import_jid
+ import_state.save
+ end
end
trait :public do
diff --git a/spec/factories/shards.rb b/spec/factories/shards.rb
new file mode 100644
index 00000000000..c095fa5f0a0
--- /dev/null
+++ b/spec/factories/shards.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :shard do
+ name "default"
+ end
+end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f7c7a257075..931095936a6 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe "Admin::Users" do
+ include Spec::Support::Helpers::Features::ListRowsHelpers
+
let!(:user) do
create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
end
@@ -30,6 +32,51 @@ describe "Admin::Users" do
expect(page).to have_button('Delete user and contributions')
end
+ describe 'search and sort' do
+ before do
+ create(:user, name: 'Foo Bar')
+ create(:user, name: 'Foo Baz')
+ create(:user, name: 'Dmitriy')
+ end
+
+ it 'searches users by name' do
+ visit admin_users_path(search_query: 'Foo')
+
+ expect(page).to have_content('Foo Bar')
+ expect(page).to have_content('Foo Baz')
+ expect(page).not_to have_content('Dmitriy')
+ end
+
+ it 'sorts users by name' do
+ visit admin_users_path
+
+ sort_by('Name')
+
+ expect(first_row.text).to include('Dmitriy')
+ expect(second_row.text).to include('Foo Bar')
+ end
+
+ it 'sorts search results only' do
+ visit admin_users_path(search_query: 'Foo')
+
+ sort_by('Name')
+
+ expect(page).not_to have_content('Dmitriy')
+ expect(first_row.text).to include('Foo Bar')
+ expect(second_row.text).to include('Foo Baz')
+ end
+
+ it 'searches with respect of sorting' do
+ visit admin_users_path(sort: 'Name')
+
+ fill_in :search_query, with: 'Foo'
+ click_button('Search users')
+
+ expect(first_row.text).to include('Foo Bar')
+ expect(second_row.text).to include('Foo Baz')
+ end
+ end
+
describe 'Two-factor Authentication filters' do
it 'counts users who have enabled 2FA' do
create(:user, :two_factor)
@@ -205,75 +252,118 @@ describe "Admin::Users" do
describe 'Impersonation' do
let(:another_user) { create(:user) }
- before do
- visit admin_user_path(another_user)
- end
-
context 'before impersonating' do
- it 'shows impersonate button for other users' do
- expect(page).to have_content('Impersonate')
+ subject { visit admin_user_path(user_to_visit) }
+
+ let(:user_to_visit) { another_user }
+
+ context 'for other users' do
+ it 'shows impersonate button for other users' do
+ subject
+
+ expect(page).to have_content('Impersonate')
+ end
end
- it 'does not show impersonate button for admin itself' do
- visit admin_user_path(current_user)
+ context 'for admin itself' do
+ let(:user_to_visit) { current_user }
+
+ it 'does not show impersonate button for admin itself' do
+ subject
- expect(page).not_to have_content('Impersonate')
+ expect(page).not_to have_content('Impersonate')
+ end
end
- it 'does not show impersonate button for blocked user' do
- another_user.block
+ context 'for blocked user' do
+ before do
+ another_user.block
+ end
- visit admin_user_path(another_user)
+ it 'does not show impersonate button for blocked user' do
+ subject
+
+ expect(page).not_to have_content('Impersonate')
+ end
+ end
+
+ context 'when impersonation is disabled' do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ end
- expect(page).not_to have_content('Impersonate')
+ it 'does not show impersonate button' do
+ subject
- another_user.activate
+ expect(page).not_to have_content('Impersonate')
+ end
end
end
context 'when impersonating' do
+ subject { click_link 'Impersonate' }
+
before do
- click_link 'Impersonate'
+ visit admin_user_path(another_user)
end
it 'logs in as the user when impersonate is clicked' do
+ subject
+
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
end
it 'sees impersonation log out icon' do
- icon = first('.fa.fa-user-secret')
+ subject
+ icon = first('.fa.fa-user-secret')
expect(icon).not_to be nil
end
- it 'logs out of impersonated user back to original user' do
- find(:css, 'li.impersonation a').click
-
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
- end
+ context 'a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ end
- it 'is redirected back to the impersonated users page in the admin after stopping' do
- find(:css, 'li.impersonation a').click
+ it 'does not redirect to password change page' do
+ subject
- expect(current_path).to eq("/admin/users/#{another_user.username}")
+ expect(current_path).to eq('/')
+ end
end
end
- context 'when impersonating a user with an expired password' do
+ context 'ending impersonation' do
+ subject { find(:css, 'li.impersonation a').click }
+
before do
- another_user.update(password_expires_at: Time.now - 5.minutes)
+ visit admin_user_path(another_user)
click_link 'Impersonate'
end
- it 'does not redirect to password change page' do
- expect(current_path).to eq('/')
+ it 'logs out of impersonated user back to original user' do
+ subject
+
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
- find(:css, 'li.impersonation a').click
+ subject
expect(current_path).to eq("/admin/users/#{another_user.username}")
end
+
+ context 'a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ subject
+
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
+ end
+ end
end
end
@@ -523,4 +613,10 @@ describe "Admin::Users" do
def check_breadcrumb(content)
expect(find('.breadcrumbs-sub-title')).to have_content(content)
end
+
+ def sort_by(text)
+ page.within('.user-sort-dropdown') do
+ click_link text
+ end
+ end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 2cdd3f55b50..d96707e55fd 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -176,7 +176,7 @@ describe 'Issue Boards add issue modal filtering', :js do
it 'filters by no label' do
set_filter('label')
- click_filter_link('No Label')
+ click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
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/members/list_members_spec.rb b/spec/features/groups/members/list_members_spec.rb
index e1587a8b6a5..4ba7161601e 100644
--- a/spec/features/groups/members/list_members_spec.rb
+++ b/spec/features/groups/members/list_members_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Groups > Members > List members' do
include Select2Helper
+ include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
@@ -43,12 +44,4 @@ describe 'Groups > Members > List members' do
let(:user_with_status) { user2 }
end
end
-
- def first_row
- page.all('ul.content-list > li')[0]
- end
-
- def second_row
- page.all('ul.content-list > li')[1]
- end
end
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index 0eda2c7f26d..e2b4a491a13 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Groups > Members > Manage members' do
include Select2Helper
+ include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
@@ -119,14 +120,6 @@ describe 'Groups > Members > Manage members' do
end
end
- def first_row
- page.all('ul.content-list > li')[0]
- end
-
- def second_row
- page.all('ul.content-list > li')[1]
- end
-
def add_user(id, role)
page.within ".users-group-form" do
select2(id, from: "#user_ids", multiple: true)
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index 65989c36c1e..6eb59ef72c2 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'IDE', :js do
- describe 'sub-groups' do
+ describe 'sub-groups', :nested_groups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
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/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index ca5d506ab04..b25b1514d62 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -45,7 +45,8 @@ describe 'Dropdown label', :js do
bug_label = create(:label, project: project, title: 'bug-label')
init_label_search
- filtered_search.native.send_keys(:down, :down, :enter)
+ # navigate to the bug_label option and selects it
+ filtered_search.native.send_keys(:down, :down, :down, :enter)
expect_tokens([label_token(bug_label.title)])
expect_filtered_search_input_empty
@@ -234,12 +235,20 @@ describe 'Dropdown label', :js do
end
it 'selects `no label`' do
- find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'None').click
expect(page).not_to have_css(js_dropdown_label)
expect_tokens([label_token('none', false)])
expect_filtered_search_input_empty
end
+
+ it 'selects `any label`' do
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'Any').click
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect_tokens([label_token('any', false)])
+ expect_filtered_search_input_empty
+ end
end
describe 'input has existing content' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 35d57b3896d..a29380a180e 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -430,10 +430,10 @@ describe 'Filter issues', :js do
expect_issues_list_count(2)
- sort_toggle = find('.filtered-search-wrapper .dropdown-toggle')
+ sort_toggle = find('.filter-dropdown-container .dropdown')
sort_toggle.click
- find('.filtered-search-wrapper .dropdown-menu li a', text: 'Created date').click
+ find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click
wait_for_requests
expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title)
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index ba5b80ed04b..b4b9a589ba3 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -40,6 +40,18 @@ describe "User comments on issue", :js do
expect(page.find('pre code').text).to eq code_block_content
end
+
+ it "does not render html content in mermaid" do
+ html_content = "<img onerror=location=`javascript\\u003aalert\\u0028document.domain\\u0029` src=x>"
+ mermaid_content = "graph LR\n B-->D(#{html_content});"
+ comment = "```mermaid\n#{mermaid_content}\n```"
+
+ add_note(comment)
+
+ wait_for_requests
+
+ expect(page.find('svg.mermaid')).to have_content html_content
+ end
end
context "when editing comments" do
diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
index ca234321235..43369f7609f 100644
--- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
@@ -1,15 +1,15 @@
require 'rails_helper'
-describe 'New issue breadcrumbs' do
+describe 'New issue breadcrumb' do
let(:project) { create(:project) }
- let(:user) { project.creator }
+ let(:user) { project.creator }
before do
sign_in(user)
- visit new_project_issue_path(project)
+ visit(new_project_issue_path(project))
end
- it 'display a link to project issues and new issue pages' do
+ it 'displays link to project issues and new issue' do
page.within '.breadcrumbs' do
expect(find_link('Issues')[:href]).to end_with(project_issues_path(project))
expect(find_link('New')[:href]).to end_with(new_project_issue_path(project))
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 4771d2c6d28..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('button.dropdown-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("button.dropdown-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/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index a25d701ee35..7008b361394 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -18,7 +18,7 @@ describe 'Mermaid rendering', :js do
visit project_issue_path(project, issue)
%w[A B C D].each do |label|
- expect(page).to have_selector('svg foreignObject', text: label)
+ expect(page).to have_selector('svg text', text: label)
end
end
end
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
new file mode 100644
index 00000000000..3560b8d90bb
--- /dev/null
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'User expands diff', :js do
+ let(:project) { create(:project, :public, :repository) }
+ 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="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
+ click_link 'Click to expand it.'
+
+ wait_for_requests
+
+ expect(page).not_to have_content('Click to expand it.')
+ expect(page).to have_selector('.code')
+ end
+ end
+end
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 87613a4fdce..328f96e6ed7 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
@@ -50,7 +50,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
find('.line-resolve-btn').click
expect(page).to have_selector('.line-resolve-btn.is-active')
- expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ expect(find('.line-resolve-btn')['aria-label']).to eq("Resolved by #{user.name}")
end
page.within '.diff-content' do
@@ -243,7 +243,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
resolve_button.click
wait_for_requests
- expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ expect(resolve_button['aria-label']).to eq("Resolved by #{user.name}")
end
end
@@ -266,7 +266,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
wait_for_requests
- expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ expect(first('.line-resolve-btn')['aria-label']).to eq("Resolved by #{user.name}")
end
expect(page).to have_content('Last updated')
@@ -285,7 +285,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
wait_for_requests
resolve_buttons.each do |button|
- expect(button['data-original-title']).to eq("Resolved by #{user.name}")
+ expect(button['aria-label']).to eq("Resolved by #{user.name}")
end
page.within '.line-resolve-all-container' do
@@ -357,13 +357,12 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
resolve_button.click
wait_for_requests
- expect(resolve_button['data-original-title']).to eq("Resolved by #{user.name}")
+ expect(resolve_button['aria-label']).to eq("Resolved by #{user.name}")
end
end
- it 'shows jump to next discussion button, apart from the last one' do
- expect(page).to have_selector('.discussion-reply-holder', count: 2)
- expect(page).to have_selector('.discussion-reply-holder .discussion-next-btn', count: 1)
+ it 'shows jump to next discussion button' do
+ expect(page.all('.discussion-reply-holder', count: 2)).to all(have_selector('.discussion-next-btn'))
end
it 'displays next discussion even if hidden' do
diff --git a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
index f17acb35a5a..18d204da17a 100644
--- a/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/merge_request/user_sees_breadcrumb_links_spec.rb
@@ -1,15 +1,15 @@
require 'rails_helper'
-describe 'New merge request breadcrumbs' do
+describe 'New merge request breadcrumb' do
let(:project) { create(:project, :repository) }
- let(:user) { project.creator }
+ let(:user) { project.creator }
before do
sign_in(user)
- visit project_new_merge_request_path(project)
+ visit(project_new_merge_request_path(project))
end
- it 'display a link to project merge requests and new merge request pages' do
+ it 'displays link to project merge requests and new merge request' do
page.within '.breadcrumbs' do
expect(find_link('Merge Requests')[:href]).to end_with(project_merge_requests_path(project))
expect(find_link('New')[:href]).to end_with(project_new_merge_request_path(project))
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 74290c0fff9..fe8e0b07d39 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -29,6 +29,22 @@ describe 'Merge request > User sees deployment widget', :js do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
+
+ context 'when a user created a new merge request with the same SHA' do
+ let(:pipeline2) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: 'new-patch-1') }
+ let(:build2) { create(:ci_build, :success, pipeline: pipeline2) }
+ let(:environment2) { create(:environment, project: project) }
+ let!(:deployment2) { create(:deployment, environment: environment2, sha: sha, ref: 'new-patch-1', deployable: build2) }
+
+ it 'displays one environment which is related to the pipeline' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_selector('.js-deployment-info', count: 1)
+ expect(page).to have_content("#{environment.name}")
+ expect(page).not_to have_content("#{environment2.name}")
+ end
+ end
end
context 'when deployment failed' do
@@ -65,7 +81,20 @@ describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Deploying to #{environment.name}")
+ expect(page).to have_content("Will deploy to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
+ end
+
+ context 'when deployment was cancelled' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :canceled, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the environment name' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Failed to deploy to #{environment.name}")
expect(page).not_to have_css('.js-deploy-time')
end
end
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index 7b8c3bacfe2..4ab9a87ad4b 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -53,13 +53,11 @@ describe 'Merge request > User sees discussions', :js do
shared_examples 'a functional discussion' do
let(:discussion_id) { note.discussion_id(merge_request) }
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'is displayed' do
+ it 'is displayed' do
expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'can be replied to' do
+ it 'can be replied to' do
within(".discussion[data-discussion-id='#{discussion_id}']") do
click_button 'Reply...'
fill_in 'note[note]', with: 'Test!'
@@ -74,16 +72,21 @@ describe 'Merge request > User sees discussions', :js do
visit project_merge_request_path(project, merge_request)
end
- context 'a regular commit comment' do
- let(:note) { create(:note_on_commit, project: project) }
-
- it_behaves_like 'a functional discussion'
- end
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # context 'a regular commit comment' do
+ # let(:note) { create(:note_on_commit, project: project) }
+ #
+ # it_behaves_like 'a functional discussion'
+ # end
context 'a commit diff comment' do
let(:note) { create(:diff_note_on_commit, project: project) }
it_behaves_like 'a functional discussion'
+
+ it 'displays correct header' do
+ expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}"
+ end
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
new file mode 100644
index 00000000000..7b473faa884
--- /dev/null
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -0,0 +1,365 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Merge request > User sees merge request pipelines', :js do
+ include ProjectForksHelper
+ include TestReportsHelper
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+
+ let(:config) do
+ {
+ build: {
+ script: 'build'
+ },
+ test: {
+ script: 'test',
+ only: ['merge_requests']
+ },
+ deploy: {
+ script: 'deploy',
+ except: ['merge_requests']
+ }
+ }
+ end
+
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ stub_feature_flags(ci_merge_request_pipeline: true)
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when a user created a merge request in the parent project' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'feature',
+ target_branch: 'master')
+ end
+
+ let!(:push_pipeline) do
+ Ci::CreatePipelineService.new(project, user, ref: 'feature')
+ .execute(:push)
+ end
+
+ let!(:merge_request_pipeline) do
+ Ci::CreatePipelineService.new(project, user, ref: 'feature')
+ .execute(:merge_request, merge_request: merge_request)
+ end
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ end
+
+ it 'sees branch pipelines and merge request pipelines in correct order' do
+ page.within('.ci-table') do
+ expect(page).to have_selector('.ci-pending', count: 2)
+ expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
+ end
+ end
+
+ it 'sees the latest merge request pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{merge_request_pipeline.id}")
+ end
+ end
+
+ context 'when a user updated a merge request in the parent project' do
+ let!(:push_pipeline_2) do
+ Ci::CreatePipelineService.new(project, user, ref: 'feature')
+ .execute(:push)
+ end
+
+ let!(:merge_request_pipeline_2) do
+ Ci::CreatePipelineService.new(project, user, ref: 'feature')
+ .execute(:merge_request, merge_request: merge_request)
+ end
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ end
+
+ it 'sees branch pipelines and merge request pipelines in correct order' do
+ page.within('.ci-table') do
+ expect(page).to have_selector('.ci-pending', count: 4)
+
+ expect(all('.js-pipeline-url-link')[0])
+ .to have_content("##{merge_request_pipeline_2.id}")
+
+ expect(all('.js-pipeline-url-link')[1])
+ .to have_content("##{merge_request_pipeline.id}")
+
+ expect(all('.js-pipeline-url-link')[2])
+ .to have_content("##{push_pipeline_2.id}")
+
+ expect(all('.js-pipeline-url-link')[3])
+ .to have_content("##{push_pipeline.id}")
+ end
+ end
+
+ it 'sees merge request tag for merge request pipelines' do
+ page.within('.ci-table') do
+ expect(all('.pipeline-tags')[0])
+ .to have_content("merge request")
+
+ expect(all('.pipeline-tags')[1])
+ .to have_content("merge request")
+
+ expect(all('.pipeline-tags')[2])
+ .not_to have_content("merge request")
+
+ expect(all('.pipeline-tags')[3])
+ .not_to have_content("merge request")
+ end
+ end
+
+ it 'sees the latest merge request pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{merge_request_pipeline_2.id}")
+ end
+ end
+ end
+
+ context 'when a user merges a merge request in the parent project' do
+ before do
+ click_button 'Merge when pipeline succeeds'
+
+ wait_for_requests
+ end
+
+ context 'when merge request pipeline is pending' do
+ it 'waits the head pipeline' do
+ expect(page).to have_content('to be merged automatically when the pipeline succeeds')
+ expect(page).to have_link('Cancel automatic merge')
+ end
+ end
+
+ context 'when merge request pipeline succeeds' do
+ before do
+ merge_request_pipeline.succeed!
+
+ wait_for_requests
+ end
+
+ it 'merges the merge request' do
+ expect(page).to have_content('Merged by')
+ expect(page).to have_link('Revert')
+ end
+ end
+
+ context 'when branch pipeline succeeds' do
+ before do
+ push_pipeline.succeed!
+
+ wait_for_requests
+ end
+
+ it 'waits the head pipeline' do
+ expect(page).to have_content('to be merged automatically when the pipeline succeeds')
+ expect(page).to have_link('Cancel automatic merge')
+ end
+ end
+ end
+
+ context 'when there are no `merge_requests` keyword in .gitlab-ci.yml' do
+ let(:config) do
+ {
+ build: {
+ script: 'build'
+ },
+ test: {
+ script: 'test'
+ },
+ deploy: {
+ script: 'deploy'
+ }
+ }
+ end
+
+ it 'sees a branch pipeline in pipeline tab' do
+ page.within('.ci-table') do
+ expect(page).to have_selector('.ci-pending', count: 1)
+ expect(first('.js-pipeline-url-link')).to have_content("##{push_pipeline.id}")
+ end
+ end
+
+ it 'sees the latest branch pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{push_pipeline.id}")
+ end
+ end
+ end
+ end
+
+ context 'when a user created a merge request from a forked project to the parent project' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: forked_project,
+ target_project: project,
+ source_branch: 'feature',
+ target_branch: 'master')
+ end
+
+ let!(:push_pipeline) do
+ Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
+ .execute(:push)
+ end
+
+ let!(:merge_request_pipeline) do
+ Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
+ .execute(:merge_request, merge_request: merge_request)
+ end
+
+ let(:forked_project) { fork_project(project, user2, repository: true) }
+ let(:user2) { create(:user) }
+
+ before do
+ forked_project.add_maintainer(user2)
+
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ end
+
+ it 'sees branch pipelines and merge request pipelines in correct order' do
+ page.within('.ci-table') do
+ expect(page).to have_selector('.ci-pending', count: 2)
+ expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
+ end
+ end
+
+ it 'sees the latest merge request pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{merge_request_pipeline.id}")
+ end
+ end
+
+ it 'sees pipeline list in forked project' do
+ visit project_pipelines_path(forked_project)
+
+ expect(page).to have_selector('.ci-pending', count: 2)
+ end
+
+ context 'when a user updated a merge request from a forked project to the parent project' do
+ let!(:push_pipeline_2) do
+ Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
+ .execute(:push)
+ end
+
+ let!(:merge_request_pipeline_2) do
+ Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
+ .execute(:merge_request, merge_request: merge_request)
+ end
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ end
+
+ it 'sees branch pipelines and merge request pipelines in correct order' do
+ page.within('.ci-table') do
+ expect(page).to have_selector('.ci-pending', count: 4)
+
+ expect(all('.js-pipeline-url-link')[0])
+ .to have_content("##{merge_request_pipeline_2.id}")
+
+ expect(all('.js-pipeline-url-link')[1])
+ .to have_content("##{merge_request_pipeline.id}")
+
+ expect(all('.js-pipeline-url-link')[2])
+ .to have_content("##{push_pipeline_2.id}")
+
+ expect(all('.js-pipeline-url-link')[3])
+ .to have_content("##{push_pipeline.id}")
+ end
+ end
+
+ it 'sees merge request tag for merge request pipelines' do
+ page.within('.ci-table') do
+ expect(all('.pipeline-tags')[0])
+ .to have_content("merge request")
+
+ expect(all('.pipeline-tags')[1])
+ .to have_content("merge request")
+
+ expect(all('.pipeline-tags')[2])
+ .not_to have_content("merge request")
+
+ expect(all('.pipeline-tags')[3])
+ .not_to have_content("merge request")
+ end
+ end
+
+ it 'sees the latest merge request pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{merge_request_pipeline_2.id}")
+ end
+ end
+
+ it 'sees pipeline list in forked project' do
+ visit project_pipelines_path(forked_project)
+
+ expect(page).to have_selector('.ci-pending', count: 4)
+ end
+ end
+
+ context 'when a user merges a merge request from a forked project to the parent project' do
+ before do
+ click_button 'Merge when pipeline succeeds'
+
+ wait_for_requests
+ end
+
+ context 'when merge request pipeline is pending' do
+ it 'waits the head pipeline' do
+ expect(page).to have_content('to be merged automatically when the pipeline succeeds')
+ expect(page).to have_link('Cancel automatic merge')
+ end
+ end
+
+ context 'when merge request pipeline succeeds' do
+ before do
+ merge_request_pipeline.succeed!
+
+ wait_for_requests
+ end
+
+ it 'merges the merge request' do
+ expect(page).to have_content('Merged by')
+ expect(page).to have_link('Revert')
+ end
+ end
+
+ context 'when branch pipeline succeeds' do
+ before do
+ push_pipeline.succeed!
+
+ wait_for_requests
+ end
+
+ it 'waits the head pipeline' do
+ expect(page).to have_content('to be merged automatically when the pipeline succeeds')
+ expect(page).to have_link('Cancel automatic merge')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 582be101399..d8ebd3c92af 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -60,7 +60,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows environments link' do
wait_for_requests
- page.within('.js-pre-merge-deploy') do
+ page.within('.js-pre-deployment') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
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 e163868e8e7..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('button.dropdown-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('button.dropdown-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('button.dropdown-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/milestones/user_promotes_milestone_spec.rb b/spec/features/milestones/user_promotes_milestone_spec.rb
new file mode 100644
index 00000000000..df1bc502134
--- /dev/null
+++ b/spec/features/milestones/user_promotes_milestone_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+describe 'User promotes milestone' do
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, namespace: group) }
+ set(:milestone) { create(:milestone, project: project) }
+
+ context 'when user can admin group milestones' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ visit(project_milestones_path(project))
+ end
+
+ it "shows milestone promote button" do
+ expect(page).to have_selector('.js-promote-project-milestone-button')
+ end
+ end
+
+ context 'when user cannot admin group milestones' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit(project_milestones_path(project))
+ end
+
+ it "does not show milestone promote button" do
+ expect(page).not_to have_selector('.js-promote-project-milestone-button')
+ end
+ end
+end
diff --git a/spec/features/milestones/user_sees_breadcrumb_links_spec.rb b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb
new file mode 100644
index 00000000000..d3906ea73bd
--- /dev/null
+++ b/spec/features/milestones/user_sees_breadcrumb_links_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe 'New project milestone breadcrumb' do
+ let(:project) { create(:project) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:user) { project.creator }
+
+ before do
+ sign_in(user)
+ visit(new_project_milestone_path(project))
+ end
+
+ it 'displays link to project milestones and new project milestone' do
+ page.within '.breadcrumbs' do
+ expect(find_link('Milestones')[:href]).to end_with(project_milestones_path(project))
+ expect(find_link('New')[:href]).to end_with(new_project_milestone_path(project))
+ end
+ end
+end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index bd254caddfb..caf69796d52 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -20,7 +20,7 @@ describe 'project commit pipelines', :js do
visit pipelines_project_commit_path(project, project.commit.sha)
page.within('.table-holder') do
- expect(page).to have_content project.pipelines[0].id # pipeline ids
+ expect(page).to have_content project.ci_pipelines[0].id # pipeline ids
end
end
end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 534cfe1eb12..2159adf49fc 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -4,10 +4,9 @@ describe 'User browses commits' do
include RepoHelpers
let(:user) { create(:user) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:project) { create(:project, :public, :repository, namespace: user.namespace) }
before do
- project.add_maintainer(user)
sign_in(user)
end
@@ -127,6 +126,26 @@ describe 'User browses commits' do
.and have_selector('entry summary', text: commit.description[0..10].delete("\r\n"))
end
+ context 'when a commit links to a confidential issue' do
+ let(:confidential_issue) { create(:issue, confidential: true, title: 'Secret issue!', project: project) }
+
+ before do
+ project.repository.create_file(user, 'dummy-file', 'dummy content',
+ branch_name: 'feature',
+ message: "Linking #{confidential_issue.to_reference}")
+ end
+
+ context 'when the user cannot see confidential issues but was cached with a link', :use_clean_rails_memory_store_fragment_caching do
+ it 'does not render the confidential issue' do
+ visit project_commits_path(project, 'feature')
+ sign_in(create(:user))
+ visit project_commits_path(project, 'feature')
+
+ expect(page).not_to have_link(href: project_issue_path(project, confidential_issue))
+ end
+ end
+ end
+
context 'master branch' do
before do
visit_commits_page
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 9772a7bacac..a8a3b6910fb 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -165,8 +165,14 @@ describe 'Environment' do
context 'web terminal', :js do
before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ # Stub #terminals as it causes js-enabled feature specs to
+ # render the page incorrectly
+ #
+ # In EE we have to stub EE::Environment since it overwrites
+ # the "terminals" method.
+ allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ .to receive(:terminals) { nil }
+
visit terminal_project_environment_path(project, environment)
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 4f93e512f1d..651c02c7ecc 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -795,7 +795,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders message about job being stuck because no runners are active' do
expect(page).to have_css('.js-stuck-no-active-runner')
- expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.")
end
end
@@ -805,7 +805,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders message about job being stuck because of no runners with the specified tags' do
expect(page).to have_css('.js-stuck-with-tags')
- expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:")
end
end
@@ -815,7 +815,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders message about job being stuck because of no runners with the specified tags' do
expect(page).to have_css('.js-stuck-with-tags')
- expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ expect(page).to have_content("This job is stuck because you don't have any active runners online with any of these tags assigned to them:")
end
end
@@ -824,7 +824,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders message about job being stuck because not runners are available' do
expect(page).to have_css('.js-stuck-no-active-runner')
- expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ expect(page).to have_content("This job is stuck because you don't have any active runners that can run this job.")
end
end
@@ -834,7 +834,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders message about job being stuck because runners are offline' do
expect(page).to have_css('.js-stuck-no-runners')
- expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.")
+ expect(page).to have_content("This job is stuck because the project doesn't have any runners online assigned to it.")
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 6178f11ded7..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-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-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/labels/user_sees_breadcrumb_links_spec.rb b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb
new file mode 100644
index 00000000000..0c0501f438a
--- /dev/null
+++ b/spec/features/projects/labels/user_sees_breadcrumb_links_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe 'New project label breadcrumb' do
+ let(:project) { create(:project) }
+ let(:user) { project.creator }
+
+ before do
+ sign_in(user)
+ visit(project_labels_path(project))
+ end
+
+ it 'displays link to project labels and new project label' do
+ page.within '.breadcrumbs' do
+ expect(find_link('Labels')[:href]).to end_with(project_labels_path(project))
+ end
+ end
+end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index c2e980e75b8..cf309492808 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Project members list' do
include Select2Helper
+ include Spec::Support::Helpers::Features::ListRowsHelpers
let(:user1) { create(:user, name: 'John Doe') }
let(:user2) { create(:user, name: 'Mary Jane') }
@@ -83,14 +84,6 @@ describe 'Project members list' do
end
end
- def first_row
- page.all('ul.content-list > li')[0]
- end
-
- def second_row
- page.all('ul.content-list > li')[1]
- end
-
def add_user(id, role)
page.within ".users-project-form" do
select2(id, from: "#user_ids", multiple: true)
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 831f22a0e69..435fb229b69 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -300,7 +300,7 @@ describe 'Pages' do
let(:pipeline) do
commit_sha = project.commit('HEAD').sha
- project.pipelines.create(
+ project.ci_pipelines.create(
ref: 'HEAD',
sha: commit_sha,
source: :push,
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 049bbca958f..a37ad9c3f43 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -499,4 +499,154 @@ describe 'Pipeline', :js do
end
end
end
+
+ context 'when user sees pipeline flags in a pipeline detail page' do
+ let(:project) { create(:project, :repository) }
+
+ context 'when pipeline is latest' do
+ include_context 'pipeline builds'
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'contains badge that indicates it is the latest build' do
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_content 'latest'
+ end
+ end
+ end
+
+ context 'when pipeline has configuration errors' do
+ include_context 'pipeline builds'
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :invalid,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'contains badge that indicates errors' do
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_content 'yaml invalid'
+ end
+ end
+
+ it 'contains badge with tooltip which contains error' do
+ expect(pipeline).to have_yaml_errors
+
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_selector(
+ %Q{span[title="#{pipeline.yaml_errors}"]})
+ end
+ end
+
+ it 'contains badge that indicates failure reason' do
+ expect(page).to have_content 'error'
+ end
+
+ it 'contains badge with tooltip which contains failure reason' do
+ expect(pipeline.failure_reason?).to eq true
+
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_selector(
+ %Q{span[title="#{pipeline.present.failure_reason}"]})
+ end
+ end
+ end
+
+ context 'when pipeline is stuck' do
+ include_context 'pipeline builds'
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ before do
+ create(:ci_build, :pending, pipeline: pipeline)
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'contains badge that indicates being stuck' do
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_content 'stuck'
+ end
+ end
+ end
+
+ context 'when pipeline uses auto devops' do
+ include_context 'pipeline builds'
+
+ let(:project) { create(:project, :repository, auto_devops_attributes: { enabled: true }) }
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :auto_devops_source,
+ project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'contains badge that indicates using auto devops' do
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_content 'Auto DevOps'
+ end
+ end
+ end
+
+ context 'when pipeline runs in a merge request context' do
+ include_context 'pipeline builds'
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: merge_request.source_project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ user: user,
+ 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
+
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'contains badge that indicates merge request pipeline' do
+ page.within(all('.well-segment')[1]) do
+ expect(page).to have_content 'merge request'
+ end
+ end
+ end
+ end
end
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 401aac9478d..418e22f8c35 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -133,25 +133,102 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_selector('#mirror_direction')
end
+ it 'creates a push mirror that mirrors all branches', :js do
+ expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('0')
+
+ fill_in 'url', with: 'ssh://user@localhost/project.git'
+ select 'SSH public key', from: 'Authentication method'
+
+ select_direction
+
+ Sidekiq::Testing.fake! do
+ click_button 'Mirror repository'
+ end
+
+ project.reload
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(project.remote_mirrors.first.only_protected_branches).to eq(false)
+ end
+
+ it 'creates a push mirror that only mirrors protected branches', :js do
+ find('#only_protected_branches').click
+
+ expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('1')
+
+ fill_in 'url', with: 'ssh://user@localhost/project.git'
+ select 'SSH public key', from: 'Authentication method'
+
+ select_direction
+
+ Sidekiq::Testing.fake! do
+ click_button 'Mirror repository'
+ end
+
+ project.reload
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(project.remote_mirrors.first.only_protected_branches).to eq(true)
+ end
+
it 'generates an SSH public key on submission', :js do
fill_in 'url', with: 'ssh://user@localhost/project.git'
select 'SSH public key', from: 'Authentication method'
+ select_direction
+
+ Sidekiq::Testing.fake! do
+ click_button 'Mirror repository'
+ end
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(page).to have_selector('[title="Copy SSH public key"]')
+ end
+
+ def select_direction(direction = 'push')
direction_select = find('#mirror_direction')
# In CE, this select box is disabled, but in EE, it is enabled
if direction_select.disabled?
- expect(direction_select.value).to eq('push')
+ expect(direction_select.value).to eq(direction)
else
- direction_select.select('Push')
+ direction_select.select(direction.capitalize)
end
+ end
+ end
- Sidekiq::Testing.fake! do
- click_button 'Mirror repository'
+ 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
- expect(page).to have_content('Mirroring settings were successfully updated')
- expect(page).to have_selector('[title="Copy SSH public key"]')
+ 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
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/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 571104d3467..48a0d675f2d 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -85,7 +85,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "test"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Test").and have_content("Create Page")
+ expect(page).to have_content("test").and have_content("Create Page")
end
click_link("Home")
@@ -97,7 +97,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "api"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("Api")
+ expect(page).to have_content("Create").and have_content("api")
end
click_link("Home")
@@ -109,7 +109,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "raketasks"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("Rake")
+ expect(page).to have_content("Create").and have_content("rake")
end
end
@@ -200,7 +200,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Foo")
+ expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -215,7 +215,7 @@ describe "User creates wiki page" do
end
# Commit message field should have correct value.
- expect(page).to have_field("wiki[message]", with: "Create spaces in the name")
+ expect(page).to have_field("wiki[message]", with: "Create Spaces in the name")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
@@ -246,7 +246,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Hyphens in the name")
+ expect(page).to have_content("hyphens in the name")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -293,7 +293,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Foo")
+ expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -311,7 +311,7 @@ describe "User creates wiki page" do
it 'renders a default sidebar when there is no customized sidebar' do
visit(project_wikis_path(project))
- expect(page).to have_content('Another')
+ expect(page).to have_content('another')
expect(page).to have_content('More Pages')
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 7ad7fec922a..f76e577b0d6 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -39,11 +39,11 @@ describe 'User updates wiki page' do
end
expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
+ expect(find('.wiki-pages')).to have_content('three')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
- expect(find('.nav-text')).to have_content('Three')
+ expect(find('.nav-text')).to have_content('three')
click_on('Edit')
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 3c93d71ab00..d4691b669c1 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -38,7 +38,7 @@ describe 'User views a wiki page' do
it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
click_on('Page history')
expect(current_path).to include('one/two/three-test')
@@ -50,11 +50,11 @@ describe 'User views a wiki page' do
it 'shows an old version of a page', :js do
expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
+ expect(find('.wiki-pages')).to have_content('three')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
- expect(find('.nav-text')).to have_content('Three')
+ expect(find('.nav-text')).to have_content('three')
click_on('Edit')
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 0add129dde2..b56bb272b46 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -277,7 +277,7 @@ describe 'Project' do
end
end
- context 'for subgroups', :js do
+ context 'for subgroups', :js, :nested_groups do
let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
let(:project) { create(:project, :repository, group: subgroup) }
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/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index 3e252ddd13c..f7b270ffa8d 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -9,7 +9,7 @@
"project_id": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
- "weight": { "type": "integer" },
+ "weight": { "type": ["integer", "null"] },
"project": {
"type": "object",
"properties": {
diff --git a/spec/fixtures/api/schemas/entities/issue_boards.json b/spec/fixtures/api/schemas/entities/issue_boards.json
new file mode 100644
index 00000000000..0ac1d9468c8
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/issue_boards.json
@@ -0,0 +1,15 @@
+{
+ "type": "object",
+ "required" : [
+ "issues",
+ "size"
+ ],
+ "properties" : {
+ "issues": {
+ "type": "array",
+ "items": { "$ref": "issue_board.json" }
+ },
+ "size": { "type": "integer" }
+ },
+ "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 4b47e259c0f..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
@@ -1,46 +1,178 @@
[
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4387",
- "url": "http://struts.apache.org/docs/s2-011.html",
- "message": "Long parameter name DoS for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "io.netty/netty - CVE-2014-3488",
+ "message": "DoS by CPU exhaustion when using malicious SSL packets",
+ "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488",
+ "severity": "Unknown",
+ "solution": "Upgrade to the latest version",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
+ "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
+ "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2014-3488",
+ "value": "CVE-2014-3488",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://bugzilla.redhat.com/CVE-2014-3488"
+ },
+ {
+ "url": "http://netty.io/news/2014/06/11/3.html"
+ },
+ {
+ "url": "https://github.com/netty/netty/issues/2562"
+ }
],
+ "priority": "Unknown",
+ "file": "app/pom.xml",
+ "url": "https://bugzilla.redhat.com/CVE-2014-3488",
"tool": "gemnasium"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-1966",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "Django - CVE-2017-12794",
+ "message": "Possible XSS in traceback section of technical 500 debug page",
+ "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version or apply patch.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f",
+ "value": "6162a015-8635-4a15-8d7c-dc9321db366f",
+ "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-12794",
+ "value": "CVE-2017-12794",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/"
+ }
],
+ "priority": "Unknown",
+ "file": "app/requirements.txt",
+ "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/",
"tool": "gemnasium"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-2115",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "nokogiri - USN-3424-1",
+ "message": "Vulnerabilities in libxml2",
+ "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
+ "value": "06565b64-486d-4326-b906-890d9915804d",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "usn",
+ "name": "USN-3424-1",
+ "value": "USN-3424-1",
+ "url": "https://usn.ubuntu.com/3424-1/"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
+ }
],
+ "priority": "Unknown",
+ "file": "rails/Gemfile.lock",
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673",
"tool": "gemnasium"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-2134",
- "url": "http://struts.apache.org/docs/s2-015.html",
- "message": "Arbitrary OGNL code execution via unsanitized wildcard matching for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "ffi - CVE-2018-1000201",
+ "message": "ruby-ffi DDL loading issue on Windows OS",
+ "cve": "ffi:1.9.18:CVE-2018-1000201",
+ "severity": "High",
+ "solution": "upgrade to \u003e= 1.9.24",
+ "scanner": {
+ "id": "bundler_audit",
+ "name": "bundler-audit"
+ },
+ "location": {
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "cve",
+ "name": "CVE-2018-1000201",
+ "value": "CVE-2018-1000201",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
+ }
],
- "tool": "gemnasium"
+ "links": [
+ {
+ "url": "https://github.com/ffi/ffi/releases/tag/1.9.24"
+ }
+ ],
+ "priority": "High",
+ "file": "sast-sample-rails/Gemfile.lock",
+ "url": "https://github.com/ffi/ffi/releases/tag/1.9.24",
+ "tool": "bundler_audit"
}
]
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 b4e4e8e7dd5..ce66f562175 100644
--- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
@@ -1,35 +1,178 @@
[
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4386",
- "url": "http://struts.apache.org/docs/s2-010.html",
- "message": "CSRF protection bypass for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "io.netty/netty - CVE-2014-3488",
+ "message": "DoS by CPU exhaustion when using malicious SSL packets",
+ "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488",
+ "severity": "Unknown",
+ "solution": "Upgrade to the latest version",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "app/pom.xml",
+ "dependency": {
+ "package": {
+ "name": "io.netty/netty"
+ },
+ "version": "3.9.1.Final"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
+ "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
+ "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2014-3488",
+ "value": "CVE-2014-3488",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://bugzilla.redhat.com/CVE-2014-3488"
+ },
+ {
+ "url": "http://netty.io/news/2014/06/11/3.html"
+ },
+ {
+ "url": "https://github.com/netty/netty/issues/2562"
+ }
],
+ "priority": "Unknown",
+ "file": "app/pom.xml",
+ "url": "https://bugzilla.redhat.com/CVE-2014-3488",
"tool": "gemnasium"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4387",
- "url": "http://struts.apache.org/docs/s2-011.html",
- "message": "Long parameter name DoS for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "Django - CVE-2017-12794",
+ "message": "Possible XSS in traceback section of technical 500 debug page",
+ "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version or apply patch.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "app/requirements.txt",
+ "dependency": {
+ "package": {
+ "name": "Django"
+ },
+ "version": "1.11.3"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f",
+ "value": "6162a015-8635-4a15-8d7c-dc9321db366f",
+ "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-12794",
+ "value": "CVE-2017-12794",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794"
+ }
],
+ "links": [
+ {
+ "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/"
+ }
+ ],
+ "priority": "Unknown",
+ "file": "app/requirements.txt",
+ "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/",
"tool": "gemnasium"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-1966",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "category": "dependency_scanning",
+ "name": "nokogiri - USN-3424-1",
+ "message": "Vulnerabilities in libxml2",
+ "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
+ "value": "06565b64-486d-4326-b906-890d9915804d",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "usn",
+ "name": "USN-3424-1",
+ "value": "USN-3424-1",
+ "url": "https://usn.ubuntu.com/3424-1/"
+ }
],
+ "links": [
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
+ }
+ ],
+ "priority": "Unknown",
+ "file": "rails/Gemfile.lock",
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673",
"tool": "gemnasium"
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "ffi - CVE-2018-1000201",
+ "message": "ruby-ffi DDL loading issue on Windows OS",
+ "cve": "ffi:1.9.18:CVE-2018-1000201",
+ "severity": "High",
+ "solution": "upgrade to \u003e= 1.9.24",
+ "scanner": {
+ "id": "bundler_audit",
+ "name": "bundler-audit"
+ },
+ "location": {
+ "file": "sast-sample-rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "ffi"
+ },
+ "version": "1.9.18"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "cve",
+ "name": "CVE-2018-1000201",
+ "value": "CVE-2018-1000201",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/ffi/ffi/releases/tag/1.9.24"
+ }
+ ],
+ "priority": "High",
+ "file": "sast-sample-rails/Gemfile.lock",
+ "url": "https://github.com/ffi/ffi/releases/tag/1.9.24",
+ "tool": "bundler_audit"
}
]
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
new file mode 100644
index 00000000000..6d73977a891
--- /dev/null
+++ b/spec/frontend/.eslintrc.yml
@@ -0,0 +1,9 @@
+---
+env:
+ jest/globals: true
+plugins:
+- jest
+settings:
+ import/resolver:
+ jest:
+ jestConfigFile: "config/jest.config.js"
diff --git a/spec/frontend/dummy_spec.js b/spec/frontend/dummy_spec.js
new file mode 100644
index 00000000000..2bfef25e9c6
--- /dev/null
+++ b/spec/frontend/dummy_spec.js
@@ -0,0 +1 @@
+it('does nothing', () => {});
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 976b6c312b4..a857b7646b2 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -471,6 +471,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..cba0d93e144
--- /dev/null
+++ b/spec/helpers/sorting_helper_spec.rb
@@ -0,0 +1,43 @@
+# 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: {}))
+ 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 091edf13cfe..46f72214831 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -123,7 +123,7 @@ describe('Api', () => {
});
});
- describe('mergerequest', () => {
+ describe('projectMergeRequest', () => {
it('fetches a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
@@ -132,7 +132,7 @@ describe('Api', () => {
title: 'test',
});
- Api.mergeRequest(projectPath, mergeRequestId)
+ Api.projectMergeRequest(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
@@ -141,7 +141,7 @@ describe('Api', () => {
});
});
- describe('mergerequest changes', () => {
+ describe('projectMergeRequestChanges', () => {
it('fetches the changes of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
@@ -150,7 +150,7 @@ describe('Api', () => {
title: 'test',
});
- Api.mergeRequestChanges(projectPath, mergeRequestId)
+ Api.projectMergeRequestChanges(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.title).toBe('test');
})
@@ -159,7 +159,7 @@ describe('Api', () => {
});
});
- describe('mergerequest versions', () => {
+ describe('projectMergeRequestVersions', () => {
it('fetches the versions of a merge request', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
@@ -170,7 +170,7 @@ describe('Api', () => {
},
]);
- Api.mergeRequestVersions(projectPath, mergeRequestId)
+ Api.projectMergeRequestVersions(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].id).toBe(123);
@@ -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';
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index 928bf70f3a2..e46edec9abb 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();
});
});
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 1a0b7612ee9..1e2f7ff4fd8 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -13,6 +13,8 @@ describe('diffs/components/app', () => {
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
+ window.mrTabs.expandViewContainer = jasmine.createSpy();
+ window.location.hash = 'ABC_123';
// setup component
const store = createDiffsStore();
@@ -39,4 +41,15 @@ describe('diffs/components/app', () => {
it('does not show commit info', () => {
expect(vm.$el).not.toContainElement('.blob-commit-info');
});
+
+ 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);
+ });
});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 9530b50c729..b77907ff26f 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -464,7 +464,11 @@ describe('diff_file_header', () => {
propsCopy.addMergeRequestButtons = true;
propsCopy.diffFile.deleted_file = true;
- const discussionGetter = () => [diffDiscussionMock];
+ const discussionGetter = () => [
+ {
+ ...diffDiscussionMock,
+ },
+ ];
const notesModuleMock = notesModule();
notesModuleMock.getters.discussions = discussionGetter;
vm = mountComponentWithStore(Component, {
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/diff_gutter_avatars_spec.js b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
index ad2605a5c5c..cdd30919b09 100644
--- a/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/javascripts/diffs/components/diff_gutter_avatars_spec.js
@@ -89,6 +89,35 @@ describe('DiffGutterAvatars', () => {
expect(component.discussions[0].expanded).toEqual(false);
component.$store.dispatch('setInitialNotes', []);
});
+
+ it('forces expansion of all discussions', () => {
+ spyOn(component.$store, 'dispatch');
+
+ component.discussions[0].expanded = true;
+ component.discussions.push({
+ ...component.discussions[0],
+ id: '123test',
+ expanded: false,
+ });
+
+ component.toggleDiscussions();
+
+ expect(component.$store.dispatch.calls.argsFor(0)).toEqual([
+ 'toggleDiscussion',
+ {
+ discussionId: component.discussions[0].id,
+ forceExpanded: true,
+ },
+ ]);
+
+ expect(component.$store.dispatch.calls.argsFor(1)).toEqual([
+ 'toggleDiscussion',
+ {
+ discussionId: component.discussions[1].id,
+ forceExpanded: true,
+ },
+ ]);
+ });
});
});
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
index 81b66cf7c9b..b983dc35a57 100644
--- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -62,6 +62,7 @@ describe('DiffLineNoteForm', () => {
component.$nextTick(() => {
expect(component.cancelCommentForm).toHaveBeenCalledWith({
lineCode: diffLines[0].line_code,
+ fileHash: component.diffFileHash,
});
expect(component.resetAutoSave).toHaveBeenCalled();
diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js
new file mode 100644
index 00000000000..170e661beea
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import store from '~/mr_notes/stores';
+import DiffTableCell from '~/diffs/components/diff_table_cell.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffTableCell', () => {
+ const createComponent = options =>
+ createComponentWithStore(Vue.extend(DiffTableCell), store, {
+ line: diffFileMockData.highlighted_diff_lines[0],
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ ...options,
+ }).$mount();
+
+ it('does not highlight row when isHighlighted prop is false', done => {
+ const vm = createComponent({ isHighlighted: false });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights row when isHighlighted prop is true', done => {
+ const vm = createComponent({ isHighlighted: true });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
new file mode 100644
index 00000000000..97926f6625e
--- /dev/null
+++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import store from '~/mr_notes/stores';
+import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('InlineDiffTableRow', () => {
+ let vm;
+ const thisLine = diffFileMockData.highlighted_diff_lines[0];
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not add hll class to line content when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('adds hll class to lineContent when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = thisLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js
new file mode 100644
index 00000000000..311eaaaa7c8
--- /dev/null
+++ b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import { createStore } from '~/mr_notes/stores';
+import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('ParallelDiffTableRow', () => {
+ describe('when one side is empty', () => {
+ let vm;
+ const thisLine = diffFileMockData.parallel_diff_lines[0];
+ const rightLine = diffFileMockData.parallel_diff_lines[0].right;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not highlight non empty line content when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights nonempty line content when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = rightLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when both sides have content', () => {
+ let vm;
+ const thisLine = diffFileMockData.parallel_diff_lines[2];
+ const rightLine = diffFileMockData.parallel_diff_lines[2].right;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not highlight either line when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
+ expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('adds hll class to lineContent when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = rightLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
+ expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
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 acd95a3dd8b..55ce19927e0 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -22,12 +22,14 @@ import actions, {
expandAllFiles,
toggleFileDiscussions,
saveDiffDiscussion,
+ setHighlightedRow,
toggleTreeOpen,
scrollToFile,
toggleShowTreeList,
} from '~/diffs/store/actions';
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', () => {
@@ -92,6 +94,14 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('setHighlightedRow', () => {
+ it('should set lineHash and fileHash of highlightedRow', () => {
+ testAction(setHighlightedRow, 'ABC_123', {}, [
+ { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
+ ]);
+ });
+ });
+
describe('assignDiscussionsToDiff', () => {
it('should merge discussions into diffs', done => {
const state = {
@@ -310,13 +320,13 @@ describe('DiffsStoreActions', () => {
describe('showCommentForm', () => {
it('should call mutation to show comment form', done => {
- const payload = { lineCode: 'lineCode' };
+ const payload = { lineCode: 'lineCode', fileHash: 'hash' };
testAction(
showCommentForm,
payload,
{},
- [{ type: types.ADD_COMMENT_FORM_LINE, payload }],
+ [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: true } }],
[],
done,
);
@@ -325,13 +335,13 @@ describe('DiffsStoreActions', () => {
describe('cancelCommentForm', () => {
it('should call mutation to cancel comment form', done => {
- const payload = { lineCode: 'lineCode' };
+ const payload = { lineCode: 'lineCode', fileHash: 'hash' };
testAction(
cancelCommentForm,
payload,
{},
- [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }],
+ [{ type: types.TOGGLE_LINE_HAS_FORM, payload: { ...payload, hasForm: false } }],
[],
done,
);
@@ -370,27 +380,50 @@ describe('DiffsStoreActions', () => {
describe('loadCollapsedDiff', () => {
it('should fetch data and call mutation with response and the give parameter', done => {
- const file = { hash: 123, loadCollapsedDiffUrl: '/load/collapsed/diff/url' };
+ const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
const mock = new MockAdapter(axios);
+ const commit = jasmine.createSpy('commit');
mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
- testAction(
- loadCollapsedDiff,
- file,
- {},
- [
- {
- type: types.ADD_COLLAPSED_DIFFS,
- payload: { file, data },
- },
- ],
- [],
- () => {
+ loadCollapsedDiff({ commit, getters: { commitId: null } }, file)
+ .then(() => {
+ expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data });
+
mock.restore();
done();
- },
- );
+ })
+ .catch(done.fail);
+ });
+
+ it('should fetch data without commit ID', () => {
+ const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' };
+ const getters = {
+ commitId: null,
+ };
+
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
+
+ loadCollapsedDiff({ commit() {}, getters }, file);
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: { commit_id: null },
+ });
+ });
+
+ it('should fetch data with commit ID', () => {
+ const file = { load_collapsed_diff_url: '/load/collapsed/diff/url' };
+ const getters = {
+ commitId: '123',
+ };
+
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
+
+ loadCollapsedDiff({ commit() {}, getters }, file);
+
+ expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
+ params: { commit_id: '123' },
+ });
});
});
@@ -469,7 +502,7 @@ describe('DiffsStoreActions', () => {
describe('scrollToLineIfNeededInline', () => {
const lineMock = {
- lineCode: 'ABC_123',
+ line_code: 'ABC_123',
};
it('should not call handleLocationHash when there is not hash', () => {
@@ -520,7 +553,7 @@ describe('DiffsStoreActions', () => {
const lineMock = {
left: null,
right: {
- lineCode: 'ABC_123',
+ line_code: 'ABC_123',
},
};
@@ -575,11 +608,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':
@@ -593,11 +633,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);
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index eef95c823fb..582535e0a53 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -186,77 +186,6 @@ describe('Diffs Module Getters', () => {
});
});
- describe('shouldRenderParallelCommentRow', () => {
- let line;
-
- beforeEach(() => {
- line = {};
-
- discussionMock.expanded = true;
-
- line.left = {
- line_code: 'ABC',
- discussions: [discussionMock],
- };
-
- line.right = {
- line_code: 'DEF',
- discussions: [discussionMock1],
- };
- });
-
- it('returns true when discussion is expanded', () => {
- expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true);
- });
-
- it('returns false when no discussion was found', () => {
- line.left.discussions = [];
- line.right.discussions = [];
-
- localState.diffLineCommentForms.ABC = false;
- localState.diffLineCommentForms.DEF = false;
-
- expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(false);
- });
-
- it('returns true when discussionForm was found', () => {
- localState.diffLineCommentForms.ABC = {};
-
- expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true);
- });
- });
-
- describe('shouldRenderInlineCommentRow', () => {
- let line;
-
- beforeEach(() => {
- discussionMock.expanded = true;
-
- line = {
- lineCode: 'ABC',
- discussions: [discussionMock],
- };
- });
-
- it('returns true when diffLineCommentForms has form', () => {
- localState.diffLineCommentForms.ABC = {};
-
- expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true);
- });
-
- it('returns false when no line discussions were found', () => {
- line.discussions = [];
-
- expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(false);
- });
-
- it('returns true if all found discussions are expanded', () => {
- discussionMock.expanded = true;
-
- expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true);
- });
- });
-
describe('getDiffFileDiscussions', () => {
it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => {
discussionMock.diff_file.file_hash = diffFileMock.file_hash;
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 598d723c940..23e8761bc55 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -55,32 +55,6 @@ describe('DiffsStoreMutations', () => {
});
});
- describe('ADD_COMMENT_FORM_LINE', () => {
- it('should set a truthy reference for the given line code in diffLineCommentForms', () => {
- const state = { diffLineCommentForms: {} };
- const lineCode = 'FDE';
-
- mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
-
- expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
- });
- });
-
- describe('REMOVE_COMMENT_FORM_LINE', () => {
- it('should remove given reference from diffLineCommentForms', () => {
- const state = { diffLineCommentForms: {} };
- const lineCode = 'FDE';
-
- mutations[types.ADD_COMMENT_FORM_LINE](state, { lineCode });
-
- expect(state.diffLineCommentForms[lineCode]).toBeTruthy();
-
- mutations[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode });
-
- expect(state.diffLineCommentForms[lineCode]).toBeUndefined();
- });
- });
-
describe('EXPAND_ALL_FILES', () => {
it('should change the collapsed prop from diffFiles', () => {
const diffFile = {
@@ -98,7 +72,9 @@ describe('DiffsStoreMutations', () => {
it('should call utils.addContextLines with proper params', () => {
const options = {
lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
- contextLines: [{ old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [] }],
+ contextLines: [
+ { old_line: 1, new_line: 1, line_code: 'ff9200_1_1', discussions: [], hasForm: false },
+ ],
fileHash: 'ff9200',
params: {
bottom: true,
@@ -223,6 +199,84 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1);
});
+ it('should not duplicate discussions on line', () => {
+ 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,
+ 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);
+ });
+
it('should add legacy discussions to the given line', () => {
const diffPosition = {
base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910',
@@ -383,4 +437,45 @@ describe('DiffsStoreMutations', () => {
expect(state.currentDiffFileId).toBe('somefileid');
});
});
+
+ describe('Set highlighted row', () => {
+ it('sets highlighted row', () => {
+ const state = createState();
+
+ mutations[types.SET_HIGHLIGHTED_ROW](state, 'ABC_123');
+
+ expect(state.highlightedRow).toBe('ABC_123');
+ });
+ });
+
+ describe('TOGGLE_LINE_HAS_FORM', () => {
+ it('sets hasForm on lines', () => {
+ const file = {
+ file_hash: 'hash',
+ parallel_diff_lines: [
+ { left: { line_code: '123', hasForm: false }, right: {} },
+ { left: {}, right: { line_code: '124', hasForm: false } },
+ ],
+ highlighted_diff_lines: [
+ { line_code: '123', hasForm: false },
+ { line_code: '124', hasForm: false },
+ ],
+ };
+ const state = {
+ diffFiles: [file],
+ };
+
+ mutations[types.TOGGLE_LINE_HAS_FORM](state, {
+ lineCode: '123',
+ hasForm: true,
+ fileHash: 'hash',
+ });
+
+ expect(file.highlighted_diff_lines[0].hasForm).toBe(true);
+ expect(file.highlighted_diff_lines[1].hasForm).toBe(false);
+
+ expect(file.parallel_diff_lines[0].left.hasForm).toBe(true);
+ expect(file.parallel_diff_lines[1].right.hasForm).toBe(false);
+ });
+ });
});
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/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
index 04925d37ac4..9e2ba1f5ce9 100644
--- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
@@ -14,10 +14,14 @@ import testAction from '../../../../helpers/vuex_action_helper';
describe('IDE merge requests actions', () => {
let mockedState;
+ let mockedRootState;
let mock;
beforeEach(() => {
mockedState = state();
+ mockedRootState = {
+ currentProjectId: 7,
+ };
mock = new MockAdapter(axios);
});
@@ -86,13 +90,16 @@ describe('IDE merge requests actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests);
+ mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests);
});
it('calls API with params', () => {
const apiSpy = spyOn(axios, 'get').and.callThrough();
- fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' });
+ fetchMergeRequests(
+ { dispatch() {}, state: mockedState, rootState: mockedRootState },
+ { type: 'created' },
+ );
expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
params: {
@@ -107,7 +114,7 @@ describe('IDE merge requests actions', () => {
const apiSpy = spyOn(axios, 'get').and.callThrough();
fetchMergeRequests(
- { dispatch() {}, state: mockedState },
+ { dispatch() {}, state: mockedState, rootState: mockedRootState },
{ type: 'created', search: 'testing search' },
);
@@ -139,6 +146,49 @@ describe('IDE merge requests actions', () => {
});
});
+ describe('success without type', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests);
+ });
+
+ it('calls API with project', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchMergeRequests(
+ { dispatch() {}, state: mockedState, rootState: mockedRootState },
+ { type: null, search: 'testing search' },
+ );
+
+ expect(apiSpy).toHaveBeenCalledWith(
+ jasmine.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`),
+ {
+ params: {
+ state: 'opened',
+ search: 'testing search',
+ },
+ },
+ );
+ });
+
+ it('dispatches success with received data', done => {
+ testAction(
+ fetchMergeRequests,
+ { type: null },
+ { ...mockedState, ...mockedRootState },
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ {
+ type: 'receiveMergeRequestsSuccess',
+ payload: mergeRequests,
+ },
+ ],
+ done,
+ );
+ });
+ });
+
describe('error', () => {
beforeEach(() => {
mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index fcf3780f0ea..ba5d672f189 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -160,9 +160,7 @@ describe('Job App ', () => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
- expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
- "This job is stuck, because you don't have any active runners that can run this job.",
- );
+ expect(vm.$el.querySelector('.js-job-stuck .js-stuck-no-active-runner')).not.toBeNull();
done();
}, 0);
});
@@ -195,9 +193,7 @@ describe('Job App ', () => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
- expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
- "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
- );
+ expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull();
done();
}, 0);
});
@@ -230,9 +226,7 @@ describe('Job App ', () => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
- expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
- "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
- );
+ expect(vm.$el.querySelector('.js-job-stuck .js-stuck-with-tags')).not.toBeNull();
done();
}, 0);
});
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/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js
index c7f4092911c..e4df8441793 100644
--- a/spec/javascripts/lib/utils/url_utility_spec.js
+++ b/spec/javascripts/lib/utils/url_utility_spec.js
@@ -1,4 +1,4 @@
-import { webIDEUrl } from '~/lib/utils/url_utility';
+import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility';
describe('URL utility', () => {
describe('webIDEUrl', () => {
@@ -26,4 +26,26 @@ describe('URL utility', () => {
});
});
});
+
+ describe('mergeUrlParams', () => {
+ it('adds w', () => {
+ expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag');
+ });
+
+ it('updates w', () => {
+ expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag');
+ });
+
+ it('adds multiple params', () => {
+ expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag');
+ });
+
+ it('adds and updates encoded params', () => {
+ expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag');
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 4cc18afdf24..59d6d4f3a7f 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -5,6 +5,7 @@ import {
deploymentData,
convertDatesMultipleSeries,
singleRowMetricsMultipleSeries,
+ queryWithoutData,
} from './mock_data';
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
@@ -104,4 +105,23 @@ describe('Graph', () => {
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
});
+
+ describe('Without data to display', () => {
+ it('shows a "no data to display" empty state on a graph', done => {
+ const component = createComponent({
+ graphData: queryWithoutData,
+ deploymentData,
+ tagsPath,
+ projectPath,
+ });
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.js-no-data-to-display text').textContent.trim(),
+ ).toEqual('No data to display');
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6c833b17f98..18ad9843d22 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = {
queries: [
{
query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
- y_label: 'Memory',
+ label: 'Memory',
unit: 'MiB',
result: [
{
@@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = {
],
},
{
+ id: 6,
title: 'CPU usage',
weight: 1,
queries: [
{
query_range:
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
+ label: 'Core Usage',
+ unit: 'Cores',
result: [
{
metric: {},
@@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = {
},
],
},
+ {
+ group: 'NGINX',
+ priority: 2,
+ metrics: [
+ {
+ id: 100,
+ title: 'Http Error Rate',
+ weight: 100,
+ queries: [
+ {
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
+ label: '5xx errors',
+ unit: '%',
+ result: [
+ {
+ metric: {},
+ values: [
+ [1495700554.925, NaN],
+ [1495700614.925, NaN],
+ [1495700674.925, NaN],
+ [1495700734.925, NaN],
+ [1495700794.925, NaN],
+ [1495700854.925, NaN],
+ [1495700914.925, NaN],
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
],
last_update: '2017-05-25T13:18:34.949Z',
};
@@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [
},
];
+export const queryWithoutData = {
+ title: 'HTTP Error rate',
+ weight: 10,
+ y_label: 'Http Error Rate',
+ queries: [
+ {
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
+ label: '5xx errors',
+ unit: '%',
+ result: [],
+ },
+ ],
+};
+
export function convertDatesMultipleSeries(multipleSeries) {
const convertedMultiple = multipleSeries;
multipleSeries.forEach((column, index) => {
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index bf68c911549..d8a980c874d 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -1,31 +1,35 @@
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData, environmentData } from './mock_data';
-describe('MonitoringStore', function() {
- this.store = new MonitoringStore();
- this.store.storeMetrics(MonitoringMock.data);
-
- it('contains one group that contains two queries sorted by priority', () => {
- expect(this.store.groups).toBeDefined();
- expect(this.store.groups.length).toEqual(1);
- expect(this.store.groups[0].metrics.length).toEqual(2);
+describe('MonitoringStore', () => {
+ const store = new MonitoringStore();
+ store.storeMetrics(MonitoringMock.data);
+
+ it('contains two groups that contains, one of which has two queries sorted by priority', () => {
+ expect(store.groups).toBeDefined();
+ expect(store.groups.length).toEqual(2);
+ expect(store.groups[0].metrics.length).toEqual(2);
});
it('gets the metrics count for every group', () => {
- expect(this.store.getMetricsCount()).toEqual(2);
+ expect(store.getMetricsCount()).toEqual(3);
});
it('contains deployment data', () => {
- this.store.storeDeploymentData(deploymentData);
+ store.storeDeploymentData(deploymentData);
- expect(this.store.deploymentData).toBeDefined();
- expect(this.store.deploymentData.length).toEqual(3);
- expect(typeof this.store.deploymentData[0]).toEqual('object');
+ expect(store.deploymentData).toBeDefined();
+ expect(store.deploymentData.length).toEqual(3);
+ expect(typeof store.deploymentData[0]).toEqual('object');
});
it('only stores environment data that contains deployments', () => {
- this.store.storeEnvironmentsData(environmentData);
+ store.storeEnvironmentsData(environmentData);
+
+ expect(store.environmentsData.length).toEqual(2);
+ });
- expect(this.store.environmentsData.length).toEqual(2);
+ it('removes the data if all the values from a query are not defined', () => {
+ expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
});
});
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index 95461396f10..0752bd05904 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -17,7 +17,7 @@ describe('diff_with_note', () => {
};
const selectors = {
get container() {
- return vm.$refs.fileHolder;
+ return vm.$el;
},
get diffTable() {
return this.container.querySelector('.diff-content table');
@@ -70,7 +70,6 @@ describe('diff_with_note', () => {
it('shows image diff', () => {
vm = mountComponentWithStore(Component, { props, store });
- expect(selectors.container).toHaveClass('js-image-file');
expect(selectors.diffTable).not.toExist();
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 4b4403689d9..ab9c52346d6 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -6,7 +6,6 @@ import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
-const diffDiscussionFixture = 'merge_requests/diff_discussion.json';
describe('noteable_discussion component', () => {
const Component = Vue.extend(noteableDiscussion);
@@ -79,88 +78,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('computed', () => {
- describe('hasMultipleUnresolvedDiscussions', () => {
- it('is false if there are no unresolved discussions', done => {
- spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([]);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('is false if there is one unresolved discussion', done => {
- spyOnProperty(vm, 'unresolvedDiscussions').and.returnValue([discussionMock]);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.hasMultipleUnresolvedDiscussions).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('is true if there are two unresolved discussions', done => {
- const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
- discussion.notes[0].resolved = false;
- vm.$store.dispatch('setInitialNotes', [discussion, discussion]);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.hasMultipleUnresolvedDiscussions).toBe(true);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('isRepliesCollapsed', () => {
- it('should return false for diff discussions', done => {
- const diffDiscussion = getJSONFixture(diffDiscussionFixture)[0];
- vm.$store.dispatch('setInitialNotes', [diffDiscussion]);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.isRepliesCollapsed).toEqual(false);
- expect(vm.$el.querySelector('.js-toggle-replies')).not.toBeNull();
- expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should return false if discussion does not have a reply', () => {
- const discussion = { ...discussionMock, resolved: true };
- discussion.notes = discussion.notes.slice(0, 1);
- const noRepliesVm = new Component({
- store,
- propsData: { discussion },
- }).$mount();
-
- expect(noRepliesVm.isRepliesCollapsed).toEqual(false);
- expect(noRepliesVm.$el.querySelector('.js-toggle-replies')).toBeNull();
- expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull();
- noRepliesVm.$destroy();
- });
-
- it('should return true for resolved non-diff discussion which has replies', () => {
- const discussion = { ...discussionMock, resolved: true };
- const resolvedDiscussionVm = new Component({
- store,
- propsData: { discussion },
- }).$mount();
-
- expect(resolvedDiscussionVm.isRepliesCollapsed).toEqual(true);
- expect(resolvedDiscussionVm.$el.querySelector('.js-toggle-replies')).not.toBeNull();
- expect(vm.$el.querySelector('.discussion-reply-holder')).not.toBeNull();
- resolvedDiscussionVm.$destroy();
- });
- });
- });
-
describe('methods', () => {
describe('jumpToNextDiscussion', () => {
it('expands next unresolved discussion', done => {
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index fcdd834e4a0..24c2b3e6570 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import $ from 'jquery';
import _ from 'underscore';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions';
@@ -330,10 +331,14 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
+
+ $('body').attr('data-page', '');
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+
+ $('body').attr('data-page', '');
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => {
@@ -353,6 +358,39 @@ describe('Actions Notes Store', () => {
{
type: 'updateMergeRequestWidget',
},
+ {
+ type: 'updateResolvableDiscussonsCounts',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches removeDiscussionsFromDiff on merge request page', done => {
+ const note = { path: `${gl.TEST_HOST}`, id: 1 };
+
+ $('body').attr('data-page', 'projects:merge_requests:show');
+
+ testAction(
+ actions.deleteNote,
+ note,
+ store.state,
+ [
+ {
+ type: 'DELETE_NOTE',
+ payload: note,
+ },
+ ],
+ [
+ {
+ type: 'updateMergeRequestWidget',
+ },
+ {
+ type: 'updateResolvableDiscussonsCounts',
+ },
+ {
+ type: 'diffs/removeDiscussionsFromDiff',
+ },
],
done,
);
@@ -399,6 +437,9 @@ describe('Actions Notes Store', () => {
{
type: 'startTaskList',
},
+ {
+ type: 'updateResolvableDiscussonsCounts',
+ },
],
done,
);
@@ -472,6 +513,9 @@ describe('Actions Notes Store', () => {
],
[
{
+ type: 'updateResolvableDiscussonsCounts',
+ },
+ {
type: 'updateMergeRequestWidget',
},
],
@@ -494,6 +538,9 @@ describe('Actions Notes Store', () => {
],
[
{
+ type: 'updateResolvableDiscussonsCounts',
+ },
+ {
type: 'updateMergeRequestWidget',
},
],
@@ -525,4 +572,17 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('updateResolvableDiscussonsCounts', () => {
+ it('commits UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', done => {
+ testAction(
+ actions.updateResolvableDiscussonsCounts,
+ null,
+ {},
+ [{ type: 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS' }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index f853f9ff088..c066975a43b 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -117,17 +117,15 @@ describe('Getters Notes Store', () => {
describe('allResolvableDiscussions', () => {
it('should return only resolvable discussions in same order', () => {
- const localGetters = {
- allDiscussions: [
- discussion3,
- unresolvableDiscussion,
- discussion1,
- unresolvableDiscussion,
- discussion2,
- ],
- };
+ state.discussions = [
+ discussion3,
+ unresolvableDiscussion,
+ discussion1,
+ unresolvableDiscussion,
+ discussion2,
+ ];
- expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([
+ expect(getters.allResolvableDiscussions(state)).toEqual([
discussion3,
discussion1,
discussion2,
@@ -135,11 +133,9 @@ describe('Getters Notes Store', () => {
});
it('should return empty array if there are no resolvable discussions', () => {
- const localGetters = {
- allDiscussions: [unresolvableDiscussion, unresolvableDiscussion],
- };
+ state.discussions = [unresolvableDiscussion, unresolvableDiscussion];
- expect(getters.allResolvableDiscussions(state, localGetters)).toEqual([]);
+ expect(getters.allResolvableDiscussions(state)).toEqual([]);
});
});
@@ -236,7 +232,7 @@ describe('Getters Notes Store', () => {
it('should return the ID of the discussion after the ID provided', () => {
expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456');
expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789');
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe(undefined);
+ expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123');
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 461de5a3106..52cdc16353a 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -297,6 +297,16 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].expanded).toEqual(false);
});
+
+ it('forces a discussions expanded state', () => {
+ const state = {
+ discussions: [{ ...discussionMock, expanded: false }],
+ };
+
+ mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id, forceExpanded: true });
+
+ expect(state.discussions[0].expanded).toEqual(true);
+ });
});
describe('UPDATE_NOTE', () => {
@@ -437,4 +447,51 @@ describe('Notes Store mutations', () => {
expect(state.commentsDisabled).toEqual(true);
});
});
+
+ 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', () => {
+ 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 }] },
+ ],
+ unresolvedDiscussionsCount: 0,
+ };
+
+ mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state);
+
+ expect(state.unresolvedDiscussionsCount).toBe(1);
+ });
+
+ it('updates hasUnresolvedDiscussions', () => {
+ 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 }] },
+ ],
+ hasUnresolvedDiscussions: 0,
+ };
+
+ mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state);
+
+ expect(state.hasUnresolvedDiscussions).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js
index 88e1789184d..1cdb0aff524 100644
--- a/spec/javascripts/pipelines/graph/job_item_spec.js
+++ b/spec/javascripts/pipelines/graph/job_item_spec.js
@@ -139,57 +139,17 @@ describe('pipeline graph job item', () => {
});
});
- describe('tooltip placement', () => {
- it('does not set tooltip boundary by default', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- });
-
- expect(component.tooltipBoundary).toBeNull();
- });
-
- it('sets tooltip boundary to viewport for small dropdowns', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- dropdownLength: 1,
- });
-
- expect(component.tooltipBoundary).toEqual('viewport');
- });
-
- it('does not set tooltip boundary for large lists', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- dropdownLength: 7,
- });
-
- expect(component.tooltipBoundary).toBeNull();
- });
- });
-
describe('for delayed job', () => {
- beforeEach(() => {
- const fifteenMinutesInMilliseconds = 900000;
- spyOn(Date, 'now').and.callFake(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - fifteenMinutesInMilliseconds,
- );
- });
-
- it('displays remaining time in tooltip', done => {
+ it('displays remaining time in tooltip', () => {
component = mountComponent(JobComponent, {
job: delayedJobFixture,
});
- Vue.nextTick()
- .then(() => {
- expect(
- component.$el
- .querySelector('.js-pipeline-graph-job-link')
- .getAttribute('data-original-title'),
- ).toEqual('delayed job - delayed manual action (00:15:00)');
- })
- .then(done)
- .catch(done.fail);
+ expect(
+ component.$el
+ .querySelector('.js-pipeline-graph-job-link')
+ .getAttribute('data-original-title'),
+ ).toEqual(`delayed job - delayed manual action (${component.remainingTime})`);
});
});
});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index d6c44f4c976..ea917b36526 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -90,7 +90,7 @@ describe('Pipeline Url Component', () => {
expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
});
- it('should render latest, yaml invalid and stuck flags when provided', () => {
+ it('should render latest, yaml invalid, merge request, and stuck flags when provided', () => {
const component = new PipelineUrlComponent({
propsData: {
pipeline: {
@@ -100,6 +100,7 @@ describe('Pipeline Url Component', () => {
latest: true,
yaml_errors: true,
stuck: true,
+ merge_request: true,
},
},
autoDevopsHelpPath: 'foo',
@@ -111,6 +112,10 @@ describe('Pipeline Url Component', () => {
'yaml invalid',
);
+ expect(component.$el.querySelector('.js-pipeline-url-mergerequest').textContent).toContain(
+ 'merge request',
+ );
+
expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js
new file mode 100644
index 00000000000..16c8c939a6f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js
@@ -0,0 +1,51 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue';
+
+const BODY_HTML = '<div class="test-body">Hello World</div>';
+const FOOTER_HTML = '<div class="test-footer">Goodbye!</div>';
+
+describe('MrWidgetContainer', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetContainer), {
+ localVue,
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has layout', () => {
+ factory();
+
+ expect(wrapper.is('.mr-widget-heading')).toBe(true);
+ expect(wrapper.contains('.mr-widget-content')).toBe(true);
+ });
+
+ it('accepts default slot', () => {
+ factory({
+ slots: {
+ default: BODY_HTML,
+ },
+ });
+
+ expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
+ });
+
+ it('accepts footer slot', () => {
+ factory({
+ slots: {
+ default: BODY_HTML,
+ footer: FOOTER_HTML,
+ },
+ });
+
+ expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
+ expect(wrapper.contains('.test-footer')).toBe(true);
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js
new file mode 100644
index 00000000000..f7c2376eebf
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js
@@ -0,0 +1,30 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const TEST_ICON = 'commit';
+
+describe('MrWidgetIcon', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetIcon), {
+ propsData: {
+ name: TEST_ICON,
+ },
+ sync: false,
+ localVue,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders icon and container', () => {
+ expect(wrapper.is('.circle-icon-container')).toBe(true);
+ expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON);
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
new file mode 100644
index 00000000000..e5155573f6f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -0,0 +1,90 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
+import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import { mockStore } from '../mock_data';
+
+describe('MrWidgetPipelineContainer', () => {
+ let wrapper;
+
+ const factory = (props = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
+ propsData: {
+ mr: Object.assign({}, mockStore),
+ ...props,
+ },
+ localVue,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when pre merge', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
+ jasmine.objectContaining({
+ pipeline: mockStore.pipeline,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.sourceBranch,
+ sourceBranchLink: mockStore.sourceBranchLink,
+ }),
+ );
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.deployments.map(dep =>
+ jasmine.objectContaining({
+ deployment: dep,
+ showMetrics: false,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+
+ describe('when post merge', () => {
+ beforeEach(() => {
+ factory({
+ isPostMerge: true,
+ });
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
+ jasmine.objectContaining({
+ pipeline: mockStore.mergePipeline,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.targetBranch,
+ sourceBranchLink: mockStore.targetBranch,
+ }),
+ );
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.postMergeDeployments.map(dep =>
+ jasmine.objectContaining({
+ deployment: dep,
+ showMetrics: true,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 17554c4fe42..072e98fc0e8 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -222,3 +222,16 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
};
+
+export const mockStore = {
+ pipeline: { id: 0 },
+ mergePipeline: { id: 1 },
+ targetBranch: 'target-branch',
+ sourceBranch: 'source-branch',
+ sourceBranchLink: 'source-branch-link',
+ deployments: [{ id: 0, name: 'bogus' }, { id: 1, name: 'bogus-docs' }],
+ postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }],
+ troubleshootingDocsPath: 'troubleshooting-docs-path',
+ ciStatus: 'ci-status',
+ hasCI: true,
+};
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/notes/timeline_entry_item_spec.js b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js
new file mode 100644
index 00000000000..c15635f2105
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe(TimelineEntryItem.name, () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(TimelineEntryItem, {
+ localVue,
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correctly', () => {
+ factory();
+
+ expect(wrapper.is('.timeline-entry')).toBe(true);
+
+ expect(wrapper.contains('.timeline-entry-inner')).toBe(true);
+ });
+
+ it('accepts default slot', () => {
+ const dummyContent = '<p>some content</p>';
+ factory({
+ slots: {
+ default: dummyContent,
+ },
+ });
+
+ const content = wrapper.find('.timeline-entry-inner :first-child');
+
+ expect(content.html()).toBe(dummyContent);
+ });
+});
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
index df24cef0b8b..91b0499375d 100644
--- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -104,5 +104,17 @@ describe Banzai::Pipeline::GfmPipeline do
expect(output).to include("src=\"test%20image.png\"")
end
+
+ it 'sanitizes the fixed link' do
+ markdown_xss = "[xss](javascript: alert%28document.domain%29)"
+ output = described_class.to_html(markdown_xss, project: project)
+
+ expect(output).not_to include("javascript")
+
+ markdown_xss = "<invalidtag>\n[xss](javascript:alert%28document.domain%29)"
+ output = described_class.to_html(markdown_xss, project: project)
+
+ expect(output).not_to include("javascript")
+ end
end
end
diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb
index 242ab4a91dd..3d979132880 100644
--- a/spec/lib/gitlab/auth/request_authenticator_spec.rb
+++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb
@@ -19,17 +19,17 @@ describe Gitlab::Auth::RequestAuthenticator do
allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user)
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
- expect(subject.user).to eq sessionless_user
+ expect(subject.user([:api])).to eq sessionless_user
end
it 'returns session user if no sessionless user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user)
- expect(subject.user).to eq session_user
+ expect(subject.user([:api])).to eq session_user
end
it 'returns nil if no user found' do
- expect(subject.user).to be_blank
+ expect(subject.user([:api])).to be_blank
end
it 'bubbles up exceptions' do
@@ -42,26 +42,26 @@ describe Gitlab::Auth::RequestAuthenticator do
let!(:feed_token_user) { build(:user) }
it 'returns access_token user first' do
- allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user)
+ allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user)
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
- expect(subject.find_sessionless_user).to eq access_token_user
+ expect(subject.find_sessionless_user([:api])).to eq access_token_user
end
it 'returns feed_token user if no access_token user found' do
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
- expect(subject.find_sessionless_user).to eq feed_token_user
+ expect(subject.find_sessionless_user([:api])).to eq feed_token_user
end
it 'returns nil if no user found' do
- expect(subject.find_sessionless_user).to be_blank
+ expect(subject.find_sessionless_user([:api])).to be_blank
end
it 'rescue Gitlab::Auth::AuthenticationError exceptions' do
- allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
+ allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError)
- expect(subject.find_sessionless_user).to be_blank
+ expect(subject.find_sessionless_user([:api])).to be_blank
end
end
end
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
index 454ad1589b9..4e4c8b215c2 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do
'rack.input' => ''
}
end
- let(:request) { Rack::Request.new(env)}
+ let(:request) { Rack::Request.new(env) }
def set_param(key, value)
request.update_param(key, value)
@@ -49,6 +49,7 @@ describe Gitlab::Auth::UserAuthFinders do
describe '#find_user_from_feed_token' do
context 'when the request format is atom' do
before do
+ env['SCRIPT_NAME'] = 'url.atom'
env['HTTP_ACCEPT'] = 'application/atom+xml'
end
@@ -56,17 +57,17 @@ describe Gitlab::Auth::UserAuthFinders do
it 'returns user if valid feed_token' do
set_param(:feed_token, user.feed_token)
- expect(find_user_from_feed_token).to eq user
+ expect(find_user_from_feed_token(:rss)).to eq user
end
it 'returns nil if feed_token is blank' do
- expect(find_user_from_feed_token).to be_nil
+ expect(find_user_from_feed_token(:rss)).to be_nil
end
it 'returns exception if invalid feed_token' do
set_param(:feed_token, 'invalid_token')
- expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
@@ -74,34 +75,38 @@ describe Gitlab::Auth::UserAuthFinders do
it 'returns user if valid rssd_token' do
set_param(:rss_token, user.feed_token)
- expect(find_user_from_feed_token).to eq user
+ expect(find_user_from_feed_token(:rss)).to eq user
end
it 'returns nil if rss_token is blank' do
- expect(find_user_from_feed_token).to be_nil
+ expect(find_user_from_feed_token(:rss)).to be_nil
end
it 'returns exception if invalid rss_token' do
set_param(:rss_token, 'invalid_token')
- expect { find_user_from_feed_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ expect { find_user_from_feed_token(:rss) }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
end
end
context 'when the request format is not atom' do
it 'returns nil' do
+ env['SCRIPT_NAME'] = 'json'
+
set_param(:feed_token, user.feed_token)
- expect(find_user_from_feed_token).to be_nil
+ expect(find_user_from_feed_token(:rss)).to be_nil
end
end
context 'when the request format is empty' do
it 'the method call does not modify the original value' do
+ env['SCRIPT_NAME'] = 'url.atom'
+
env.delete('action_dispatch.request.formats')
- find_user_from_feed_token
+ find_user_from_feed_token(:rss)
expect(env['action_dispatch.request.formats']).to be_nil
end
@@ -111,8 +116,12 @@ describe Gitlab::Auth::UserAuthFinders do
describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
+ before do
+ env['SCRIPT_NAME'] = 'url.atom'
+ end
+
it 'returns nil if no access_token present' do
- expect(find_personal_access_token).to be_nil
+ expect(find_user_from_access_token).to be_nil
end
context 'when validate_access_token! returns valid' do
@@ -131,9 +140,59 @@ describe Gitlab::Auth::UserAuthFinders do
end
end
+ describe '#find_user_from_web_access_token' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ before do
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'returns exception if token has no user' do
+ allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
+
+ expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+
+ context 'no feed or API requests' do
+ it 'returns nil if the request is not RSS' do
+ expect(find_user_from_web_access_token(:rss)).to be_nil
+ end
+
+ it 'returns nil if the request is not ICS' do
+ expect(find_user_from_web_access_token(:ics)).to be_nil
+ end
+
+ it 'returns nil if the request is not API' do
+ expect(find_user_from_web_access_token(:api)).to be_nil
+ end
+ end
+
+ it 'returns the user for RSS requests' do
+ env['SCRIPT_NAME'] = 'url.atom'
+
+ expect(find_user_from_web_access_token(:rss)).to eq(user)
+ end
+
+ it 'returns the user for ICS requests' do
+ env['SCRIPT_NAME'] = 'url.ics'
+
+ expect(find_user_from_web_access_token(:ics)).to eq(user)
+ end
+
+ it 'returns the user for API requests' do
+ env['SCRIPT_NAME'] = '/api/endpoint'
+
+ expect(find_user_from_web_access_token(:api)).to eq(user)
+ end
+ end
+
describe '#find_personal_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
+ before do
+ env['SCRIPT_NAME'] = 'url.atom'
+ end
+
context 'passed as header' do
it 'returns token if valid personal_access_token' do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
@@ -220,5 +279,20 @@ describe Gitlab::Auth::UserAuthFinders do
expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
end
end
+
+ context 'with impersonation token' do
+ let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ context 'when impersonation is disabled' do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
+ end
+
+ it 'returns Gitlab::Auth::ImpersonationDisabled' do
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::ImpersonationDisabled)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb
new file mode 100644
index 00000000000..c66d7cd6148
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_fullpath_in_repo_config_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig, :migration, schema: 20181010133639 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) }
+
+ describe described_class::Storage::HashedProject do
+ let(:project) { double(id: 555) }
+ subject(:project_storage) { described_class.new(project) }
+
+ it 'has the correct disk_path' do
+ expect(project_storage.disk_path).to eq('@hashed/91/a7/91a73fd806ab2c005c13b4dc19130a884e909dea3f72d46e30266fe1a1f588d8')
+ end
+ end
+
+ describe described_class::Storage::LegacyProject do
+ let(:project) { double(full_path: 'this/is/the/full/path') }
+ subject(:project_storage) { described_class.new(project) }
+
+ it 'has the correct disk_path' do
+ expect(project_storage.disk_path).to eq('this/is/the/full/path')
+ end
+ end
+
+ describe described_class::Project do
+ let(:project_record) { projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') }
+ subject(:project) { described_class.find(project_record.id) }
+
+ describe '#full_path' do
+ it 'returns path containing all parent namespaces' do
+ expect(project.full_path).to eq('foo/bar/baz')
+ end
+
+ it 'raises OrphanedNamespaceError when any parent namespace does not exist' do
+ subgroup.update_attribute(:parent_id, namespaces.maximum(:id).succ)
+
+ expect { project.full_path }.to raise_error(Gitlab::BackgroundMigration::BackfillProjectFullpathInRepoConfig::OrphanedNamespaceError)
+ end
+ end
+ end
+
+ describe described_class::Up do
+ describe '#perform' do
+ subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) }
+
+ it 'asks the gitaly client to set config' do
+ projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz')
+ projects.create!(namespace_id: subgroup.id, name: 'buzz', path: 'buzz', storage_version: 1)
+
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ allow(repository_service).to receive(:cleanup)
+ expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/baz')
+ end
+
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ allow(repository_service).to receive(:cleanup)
+ expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => 'foo/bar/buzz')
+ end
+
+ migrate
+ end
+ end
+ end
+
+ describe described_class::Down do
+ describe '#perform' do
+ subject(:migrate) { described_class.new.perform(projects.minimum(:id), projects.maximum(:id)) }
+
+ it 'asks the gitaly client to set config' do
+ projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz')
+
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ allow(repository_service).to receive(:cleanup)
+ expect(repository_service).to receive(:delete_config).with(['gitlab.fullpath'])
+ end
+
+ migrate
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/encrypt_runners_tokens_spec.rb b/spec/lib/gitlab/background_migration/encrypt_runners_tokens_spec.rb
new file mode 100644
index 00000000000..9d4921968b3
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/encrypt_runners_tokens_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::EncryptRunnersTokens, :migration, schema: 20181121111200 do
+ let(:settings) { table(:application_settings) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:runners) { table(:ci_runners) }
+
+ context 'when migrating application settings' do
+ before do
+ settings.create!(id: 1, runners_registration_token: 'plain-text-token1')
+ end
+
+ it 'migrates runners registration tokens' do
+ migrate!(:settings, 1, 1)
+
+ encrypted_token = settings.first.runners_registration_token_encrypted
+ decrypted_token = ::Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+
+ expect(decrypted_token).to eq 'plain-text-token1'
+ expect(settings.first.runners_registration_token).to eq 'plain-text-token1'
+ end
+ end
+
+ context 'when migrating namespaces' do
+ before do
+ namespaces.create!(id: 11, name: 'gitlab', path: 'gitlab-org', runners_token: 'my-token1')
+ namespaces.create!(id: 12, name: 'gitlab', path: 'gitlab-org', runners_token: 'my-token2')
+ namespaces.create!(id: 22, name: 'gitlab', path: 'gitlab-org', runners_token: 'my-token3')
+ end
+
+ it 'migrates runners registration tokens' do
+ migrate!(:namespace, 11, 22)
+
+ expect(namespaces.all.reload).to all(
+ have_attributes(runners_token: be_a(String), runners_token_encrypted: be_a(String))
+ )
+ end
+ end
+
+ context 'when migrating projects' do
+ before do
+ namespaces.create!(id: 11, name: 'gitlab', path: 'gitlab-org')
+ projects.create!(id: 111, namespace_id: 11, name: 'gitlab', path: 'gitlab-ce', runners_token: 'my-token1')
+ projects.create!(id: 114, namespace_id: 11, name: 'gitlab', path: 'gitlab-ce', runners_token: 'my-token2')
+ projects.create!(id: 116, namespace_id: 11, name: 'gitlab', path: 'gitlab-ce', runners_token: 'my-token3')
+ end
+
+ it 'migrates runners registration tokens' do
+ migrate!(:project, 111, 116)
+
+ expect(projects.all.reload).to all(
+ have_attributes(runners_token: be_a(String), runners_token_encrypted: be_a(String))
+ )
+ end
+ end
+
+ context 'when migrating runners' do
+ before do
+ runners.create!(id: 201, runner_type: 1, token: 'plain-text-token1')
+ runners.create!(id: 202, runner_type: 1, token: 'plain-text-token2')
+ runners.create!(id: 203, runner_type: 1, token: 'plain-text-token3')
+ end
+
+ it 'migrates runners communication tokens' do
+ migrate!(:runner, 201, 203)
+
+ expect(runners.all.reload).to all(
+ have_attributes(token: be_a(String), token_encrypted: be_a(String))
+ )
+ end
+ end
+
+ def migrate!(model, from, to)
+ subject.perform(model, from, to)
+ 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/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
new file mode 100644
index 00000000000..77366e91dca
--- /dev/null
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::BranchCheck do
+ include_context 'change access checks context'
+
+ describe '#validate!' do
+ it 'does not raise any error' do
+ expect { subject.validate! }.not_to raise_error
+ end
+
+ context 'trying to delete the default branch' do
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+ let(:ref) { 'refs/heads/master' }
+
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
+ end
+ end
+
+ context 'protected branches check' do
+ before do
+ allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true)
+ allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true)
+ end
+
+ it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
+ end
+
+ it 'raises an error if the user is not allowed to merge to protected branches' do
+ expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
+ expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
+ end
+
+ it 'raises an error if the user is not allowed to push to protected branches' do
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
+ end
+
+ context 'when project repository is empty' do
+ let(:project) { create(:project) }
+
+ it 'raises an error if the user is not allowed to push to protected branches' do
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/)
+ end
+ end
+
+ context 'branch deletion' do
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+ let(:ref) { 'refs/heads/feature' }
+
+ context 'if the user is not allowed to delete protected branches' do
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.')
+ end
+ end
+
+ context 'if the user is allowed to delete protected branches' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'through the web interface' do
+ let(:protocol) { 'web' }
+
+ it 'allows branch deletion' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ context 'over SSH or HTTP' do
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 81804ba5c76..45fb33e9e4a 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -2,245 +2,56 @@ require 'spec_helper'
describe Gitlab::Checks::ChangeAccess do
describe '#exec' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
- let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
- let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
- let(:ref) { 'refs/heads/master' }
- let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
- let(:protocol) { 'ssh' }
- let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
- let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
+ include_context 'change access checks context'
- subject(:change_access) do
- described_class.new(
- changes,
- project: project,
- user_access: user_access,
- protocol: protocol,
- logger: logger
- )
- end
-
- before do
- project.add_developer(user)
- end
+ subject { change_access }
context 'without failed checks' do
it "doesn't raise an error" do
expect { subject.exec }.not_to raise_error
end
- end
- context 'when time limit was reached' do
- it 'raises a TimeoutError' do
- logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
- access = described_class.new(changes,
- project: project,
- user_access: user_access,
- protocol: protocol,
- logger: logger)
+ it 'calls pushes checks' do
+ expect_any_instance_of(Gitlab::Checks::PushCheck).to receive(:validate!)
- expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ subject.exec
end
- end
- context 'when the user is not allowed to push to the repo' do
- it 'raises an error' do
- expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
- expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false)
+ it 'calls branches checks' do
+ expect_any_instance_of(Gitlab::Checks::BranchCheck).to receive(:validate!)
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
+ subject.exec
end
- end
- context 'tags check' do
- let(:ref) { 'refs/tags/v1.0.0' }
+ it 'calls tags checks' do
+ expect_any_instance_of(Gitlab::Checks::TagCheck).to receive(:validate!)
- it 'raises an error if the user is not allowed to update tags' do
- allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
- expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
+ subject.exec
end
- context 'with protected tag' do
- let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
-
- context 'as maintainer' do
- before do
- project.add_maintainer(user)
- end
+ it 'calls lfs checks' do
+ expect_any_instance_of(Gitlab::Checks::LfsCheck).to receive(:validate!)
- context 'deletion' do
- let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
- let(:newrev) { '0000000000000000000000000000000000000000' }
-
- it 'is prevented' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
- end
- end
-
- context 'update' do
- let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
- let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
-
- it 'is prevented' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
- end
- end
- end
-
- context 'creation' do
- let(:oldrev) { '0000000000000000000000000000000000000000' }
- let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
- let(:ref) { 'refs/tags/v9.1.0' }
-
- it 'prevents creation below access level' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
- end
-
- context 'when user has access' do
- let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
-
- it 'allows tag creation' do
- expect { subject.exec }.not_to raise_error
- end
- end
- end
+ subject.exec
end
- end
- context 'branches check' do
- context 'trying to delete the default branch' do
- let(:newrev) { '0000000000000000000000000000000000000000' }
- let(:ref) { 'refs/heads/master' }
+ it 'calls diff checks' do
+ expect_any_instance_of(Gitlab::Checks::DiffCheck).to receive(:validate!)
- it 'raises an error' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'The default branch of a project cannot be deleted.')
- end
- end
-
- context 'protected branches check' do
- before do
- allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true)
- allow(ProtectedBranch).to receive(:protected?).with(project, 'feature').and_return(true)
- end
-
- it 'raises an error if the user is not allowed to do forced pushes to protected branches' do
- expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to force push code to a protected branch on this project.')
- end
-
- it 'raises an error if the user is not allowed to merge to protected branches' do
- expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
- expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
- expect(user_access).to receive(:can_push_to_branch?).and_return(false)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to merge code into protected branches on this project.')
- end
-
- it 'raises an error if the user is not allowed to push to protected branches' do
- expect(user_access).to receive(:can_push_to_branch?).and_return(false)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
- end
-
- context 'when project repository is empty' do
- let(:project) { create(:project) }
-
- it 'raises an error if the user is not allowed to push to protected branches' do
- expect(user_access).to receive(:can_push_to_branch?).and_return(false)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/)
- end
- end
-
- context 'branch deletion' do
- let(:newrev) { '0000000000000000000000000000000000000000' }
- let(:ref) { 'refs/heads/feature' }
-
- context 'if the user is not allowed to delete protected branches' do
- it 'raises an error' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.')
- end
- end
-
- context 'if the user is allowed to delete protected branches' do
- before do
- project.add_maintainer(user)
- end
-
- context 'through the web interface' do
- let(:protocol) { 'web' }
-
- it 'allows branch deletion' do
- expect { subject.exec }.not_to raise_error
- end
- end
-
- context 'over SSH or HTTP' do
- it 'raises an error' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only delete protected branches using the web interface.')
- end
- end
- end
- end
+ subject.exec
end
end
- context 'LFS integrity check' do
- it 'fails if any LFS blobs are missing' do
- allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true)
-
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
- end
-
- it 'succeeds if LFS objects have already been uploaded' do
- allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false)
-
- expect { subject.exec }.not_to raise_error
- end
- end
-
- context 'LFS file lock check' do
- let(:owner) { create(:user) }
- let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
-
- before do
- allow(project.repository).to receive(:new_commits).and_return(
- project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
- )
- end
-
- context 'with LFS not enabled' do
- it 'skips the validation' do
- expect_any_instance_of(Gitlab::Checks::CommitCheck).not_to receive(:validate)
-
- subject.exec
- end
- end
-
- context 'with LFS enabled' do
- before do
- allow(project).to receive(:lfs_enabled?).and_return(true)
- end
-
- context 'when change is sent by a different user' do
- it 'raises an error if the user is not allowed to update the file' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
- end
- end
-
- context 'when change is sent by the author of the lock' do
- let(:user) { owner }
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
- it "doesn't raise any error" do
- expect { subject.exec }.not_to raise_error
- end
- end
+ expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
end
end
end
diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb
new file mode 100644
index 00000000000..eeec1e83179
--- /dev/null
+++ b/spec/lib/gitlab/checks/diff_check_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::DiffCheck do
+ include_context 'change access checks context'
+
+ describe '#validate!' do
+ let(:owner) { create(:user) }
+ let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
+
+ before do
+ allow(project.repository).to receive(:new_commits).and_return(
+ project.repository.commits_between('be93687618e4b132087f430a4d8fc3a609c9b77c', '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51')
+ )
+ end
+
+ context 'with LFS not enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ end
+
+ it 'skips the validation' do
+ expect(subject).not_to receive(:validate_diff)
+ expect(subject).not_to receive(:validate_file_paths)
+
+ subject.validate!
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ context 'when change is sent by a different user' do
+ it 'raises an error if the user is not allowed to update the file' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
+ end
+ end
+
+ context 'when change is sent by the author of the lock' do
+ let(:user) { owner }
+
+ it "doesn't raise any error" do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb
new file mode 100644
index 00000000000..35f8069c8a4
--- /dev/null
+++ b/spec/lib/gitlab/checks/lfs_check_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::LfsCheck do
+ include_context 'change access checks context'
+
+ let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
+
+ before do
+ allow_any_instance_of(Gitlab::Git::LfsChanges).to receive(:new_pointers) do
+ [blob_object]
+ end
+ end
+
+ describe '#validate!' do
+ context 'with LFS not enabled' do
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
+
+ subject.validate!
+ end
+ end
+
+ context 'with LFS enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ context 'deletion' do
+ let(:changes) { { oldrev: oldrev, ref: ref } }
+
+ it 'skips integrity check' do
+ expect(project.repository).not_to receive(:new_objects)
+
+ subject.validate!
+ end
+ end
+
+ it 'fails if any LFS blobs are missing' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/)
+ end
+
+ it 'succeeds if LFS objects have already been uploaded' do
+ lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
+ create(:lfs_objects_project, project: project, lfs_object: lfs_object)
+
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb
new file mode 100644
index 00000000000..25f0d428cb9
--- /dev/null
+++ b/spec/lib/gitlab/checks/push_check_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::PushCheck do
+ include_context 'change access checks context'
+
+ describe '#validate!' do
+ it 'does not raise any error' do
+ expect { subject.validate! }.not_to raise_error
+ end
+
+ context 'when the user is not allowed to push to the repo' do
+ it 'raises an error' do
+ expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
+ expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb
new file mode 100644
index 00000000000..b1258270611
--- /dev/null
+++ b/spec/lib/gitlab/checks/tag_check_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::TagCheck do
+ include_context 'change access checks context'
+
+ describe '#validate!' do
+ let(:ref) { 'refs/tags/v1.0.0' }
+
+ it 'raises an error' do
+ allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
+ expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
+ end
+
+ context 'with protected tag' do
+ let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
+
+ context 'as maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'deletion' do
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+
+ it 'is prevented' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be deleted/)
+ end
+ end
+
+ context 'update' do
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ it 'is prevented' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /cannot be updated/)
+ end
+ end
+ end
+
+ context 'creation' do
+ let(:oldrev) { '0000000000000000000000000000000000000000' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:ref) { 'refs/tags/v9.1.0' }
+
+ it 'prevents creation below access level' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /allowed to create this tag as it is protected/)
+ end
+
+ context 'when user has access' do
+ let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
+
+ it 'allows tag creation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ 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
new file mode 100644
index 00000000000..d036bf2f4d1
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/except_policy_spec.rb
@@ -0,0 +1,15 @@
+# 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 7c18514934e..12f4b9dc624 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -160,7 +160,8 @@ describe Gitlab::Ci::Config::Entry::Global do
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
variables: { 'VAR' => 'value' },
ignore: false,
- after_script: ['make clean'] },
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] } },
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
@@ -171,7 +172,8 @@ describe Gitlab::Ci::Config::Entry::Global do
cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' },
variables: {},
ignore: false,
- after_script: ['make clean'] }
+ after_script: ['make clean'],
+ only: { refs: %w[branches tags] } }
)
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 57d4577a90c..c1f4a060063 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -258,7 +258,8 @@ describe Gitlab::Ci::Config::Entry::Job do
commands: "ls\npwd\nrspec",
stage: 'test',
ignore: false,
- after_script: %w[cleanup])
+ after_script: %w[cleanup],
+ only: { refs: %w[branches tags] })
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 c0a2b6517e3..2a753408f54 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -67,12 +67,14 @@ describe Gitlab::Ci::Config::Entry::Jobs do
script: %w[rspec],
commands: 'rspec',
ignore: false,
- stage: 'test' },
+ stage: 'test',
+ only: { refs: %w[branches tags] } },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
ignore: false,
- stage: 'test' })
+ stage: 'test',
+ only: { refs: %w[branches tags] } })
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
new file mode 100644
index 00000000000..5518b68e51a
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/only_policy_spec.rb
@@ -0,0 +1,15 @@
+# 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 83001b7fdd8..cf40a22af2e 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,173 +1,8 @@
-require 'fast_spec_helper'
-require_dependency 'active_model'
+require 'spec_helper'
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/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 85d73e5c382..fab071405df 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -18,6 +18,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
before_sha: nil,
trigger_request: nil,
schedule: nil,
+ merge_request: nil,
project: project,
current_user: user,
variables_attributes: variables_attributes)
@@ -76,6 +77,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
before_sha: nil,
trigger_request: nil,
schedule: nil,
+ merge_request: nil,
project: project,
current_user: user)
end
@@ -90,4 +92,31 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline).to be_tag
end
end
+
+ context 'when pipeline is running for a merge request' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :merge_request,
+ origin_ref: 'feature',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ merge_request: merge_request,
+ project: project,
+ current_user: user)
+ end
+
+ let(:merge_request) { build(:merge_request, target_project: project) }
+
+ before do
+ step.perform!
+ end
+
+ it 'correctly indicated that this is a merge request pipeline' do
+ expect(pipeline).to be_merge_request
+ expect(pipeline.merge_request).to eq(merge_request)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index a8dc5356413..053bc421649 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -106,4 +106,34 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
expect(step.break?).to be false
end
end
+
+ context 'when pipeline source is merge request' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
+
+ let(:merge_request_pipeline) do
+ build(:ci_pipeline, source: :merge_request, project: project)
+ end
+
+ let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) }
+
+ context "when config contains 'merge_requests' keyword" do
+ let(:config) { { rspec: { script: 'echo', only: ['merge_requests'] } } }
+
+ it 'does not break the chain' do
+ expect(chain).not_to be_break
+ end
+ end
+
+ context "when config contains 'merge_request' keyword" do
+ let(:config) { { rspec: { script: 'echo', only: ['merge_request'] } } }
+
+ it 'does not break the chain' do
+ expect(chain).not_to be_break
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb
index b028b771375..abb4fff3ad7 100644
--- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/config/entry/attributable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Attributable do
+describe Gitlab::Config::Entry::Attributable do
let(:node) do
Class.new do
- include Gitlab::Ci::Config::Entry::Attributable
+ include Gitlab::Config::Entry::Attributable
end
end
@@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do
it 'raises an error' do
expectation = expect do
Class.new(String) do
- include Gitlab::Ci::Config::Entry::Attributable
+ include Gitlab::Config::Entry::Attributable
attributes :length
end
diff --git a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb b/spec/lib/gitlab/config/entry/boolean_spec.rb
index 5f067cad93c..1b7a3f850ec 100644
--- a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
+++ b/spec/lib/gitlab/config/entry/boolean_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Boolean do
+describe Gitlab::Config::Entry::Boolean do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb
index 088d4b472da..85a7cf1d241 100644
--- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
+++ b/spec/lib/gitlab/config/entry/configurable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Configurable do
+describe Gitlab::Config::Entry::Configurable do
let(:entry) do
- Class.new(Gitlab::Ci::Config::Entry::Node) do
- include Gitlab::Ci::Config::Entry::Configurable
+ Class.new(Gitlab::Config::Entry::Node) do
+ include Gitlab::Config::Entry::Configurable
end
end
@@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do
it 'creates a node factory' do
expect(entry.nodes[:object])
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory
+ .to be_an_instance_of Gitlab::Config::Entry::Factory
end
it 'returns a duplicated factory object' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index 8dd48e4efae..c29d17eaee3 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -1,9 +1,17 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Factory do
+describe Gitlab::Config::Entry::Factory do
describe '#create!' do
+ class Script < Gitlab::Config::Entry::Node
+ include Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+
+ let(:entry) { Script }
let(:factory) { described_class.new(entry) }
- let(:entry) { Gitlab::Ci::Config::Entry::Script }
context 'when setting a concrete value' do
it 'creates entry with valid value' do
@@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when not setting a value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
- Gitlab::Ci::Config::Entry::Factory::InvalidFactory
+ Gitlab::Config::Entry::Factory::InvalidFactory
)
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
index 395062207a3..bc8387ada67 100644
--- a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb
+++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Simplifiable do
+describe Gitlab::Config::Entry::Simplifiable do
describe '.strategy' do
let(:entry) do
Class.new(described_class) do
diff --git a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb
index fdf48d84192..48f9d276c95 100644
--- a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
+++ b/spec/lib/gitlab/config/entry/undefined_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Undefined do
+describe Gitlab::Config::Entry::Undefined do
let(:entry) { described_class.new }
describe '#leaf?' do
diff --git a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb
index 66f88fa35b6..64421824a12 100644
--- a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
+++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Unspecified do
+describe Gitlab::Config::Entry::Unspecified do
let(:unspecified) { described_class.new(entry) }
let(:entry) { spy('Entry') }
diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/config/entry/validatable_spec.rb
index ae2a7a51ba6..5a8f9766d23 100644
--- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
+++ b/spec/lib/gitlab/config/entry/validatable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Validatable do
+describe Gitlab::Config::Entry::Validatable do
let(:entry) do
- Class.new(Gitlab::Ci::Config::Entry::Node) do
- include Gitlab::Ci::Config::Entry::Validatable
+ Class.new(Gitlab::Config::Entry::Node) do
+ include Gitlab::Config::Entry::Validatable
end
end
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do
it 'returns validator' do
expect(entry.validator.superclass)
- .to be Gitlab::Ci::Config::Entry::Validator
+ .to be Gitlab::Config::Entry::Validator
end
it 'returns only one validator to mitigate leaks' do
diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/config/entry/validator_spec.rb
index 172b6b47a4f..efa16c4265c 100644
--- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb
+++ b/spec/lib/gitlab/config/entry/validator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Validator do
+describe Gitlab::Config::Entry::Validator do
let(:validator) { Class.new(described_class) }
let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') }
diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 590fc8904c1..44c9a3896a8 100644
--- a/spec/lib/gitlab/ci/config/loader_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Loader do
+describe Gitlab::Config::Loader::Yaml do
let(:loader) { described_class.new(yml) }
context 'when yaml syntax is correct' do
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#load!' do
it 'raises error' do
expect { loader.load! }.to raise_error(
- Gitlab::Ci::Config::Loader::FormatError,
+ Gitlab::Config::Loader::FormatError,
'Invalid configuration format'
)
end
@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#initialize' do
it 'raises FormatError' do
- expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias')
+ expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias')
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/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb
new file mode 100644
index 00000000000..05cc6cf15de
--- /dev/null
+++ b/spec/lib/gitlab/crypto_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::CryptoHelper do
+ describe '.sha256' do
+ it 'generates SHA256 digest Base46 encoded' do
+ digest = described_class.sha256('some-value')
+
+ expect(digest).to match %r{\A[A-Za-z0-9+/=]+\z}
+ expect(digest).to eq digest.strip
+ end
+ end
+
+ describe '.aes256_gcm_encrypt' do
+ it 'is Base64 encoded string without new line character' do
+ encrypted = described_class.aes256_gcm_encrypt('some-value')
+
+ expect(encrypted).to match %r{\A[A-Za-z0-9+/=]+\z}
+ expect(encrypted).not_to include "\n"
+ end
+ end
+
+ describe '.aes256_gcm_decrypt' do
+ let(:encrypted) { described_class.aes256_gcm_encrypt('some-value') }
+
+ it 'correctly decrypts encrypted string' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted)
+
+ expect(decrypted).to eq 'some-value'
+ end
+
+ it 'decrypts a value when it ends with a new line character' do
+ decrypted = described_class.aes256_gcm_decrypt(encrypted + "\n")
+
+ expect(decrypted).to eq 'some-value'
+ 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
new file mode 100644
index 00000000000..3991c737a26
--- /dev/null
+++ b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::ExactCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+
+ subject { described_class.new(models).count }
+
+ describe '#count' do
+ it 'counts all models' do
+ expect(models).to all(receive(:count).and_call_original)
+
+ 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
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is enabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
new file mode 100644
index 00000000000..b44e8c5a110
--- /dev/null
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::ReltuplesCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+ subject { described_class.new(models).count }
+
+ describe '#count', :postgresql do
+ context 'when reltuples is up to date' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ ActiveRecord::Base.connection.execute('ANALYZE identities')
+ end
+
+ it 'uses statistics to do the count' do
+ models.each { |model| expect(model).not_to receive(:count) }
+
+ expect(subject).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ context 'insufficient permissions' do
+ it 'returns an empty hash' do
+ allow(ActiveRecord::Base).to receive(:transaction).and_raise(PG::InsufficientPrivilege)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
+
+ describe '.enabled?' do
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is disabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
new file mode 100644
index 00000000000..203f9344a41
--- /dev/null
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::TablesampleCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+ let(:strategy) { described_class.new(models) }
+
+ subject { strategy.count }
+
+ describe '#count', :postgresql do
+ let(:estimates) { { Project => threshold + 1, Identity => threshold - 1 } }
+ let(:threshold) { Gitlab::Database::Count::TablesampleCountStrategy::EXACT_COUNT_THRESHOLD }
+
+ before do
+ allow(strategy).to receive(:size_estimates).with(check_statistics: false).and_return(estimates)
+ end
+
+ context 'for tables with an estimated small size' do
+ it 'performs an exact count' do
+ expect(Identity).to receive(:count).and_call_original
+
+ expect(subject).to include({ Identity => 1 })
+ end
+ end
+
+ context 'for tables with an estimated large size' do
+ it 'performs a tablesample count' do
+ expect(Project).not_to receive(:count)
+
+ result = subject
+ expect(result[Project]).to eq(3)
+ end
+ end
+
+ context 'insufficient permissions' do
+ it 'returns an empty hash' do
+ allow(strategy).to receive(:size_estimates).and_raise(PG::InsufficientPrivilege)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
+
+ describe '.enabled?' do
+ before do
+ stub_feature_flags(tablesample_counts: true)
+ end
+
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is disabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
index 407d9470785..1d096b8fa7c 100644
--- a/spec/lib/gitlab/database/count_spec.rb
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -8,63 +8,51 @@ describe Gitlab::Database::Count do
let(:models) { [Project, Identity] }
- describe '.approximate_counts' do
- context 'with MySQL' do
- context 'when reltuples have not been updated' do
- it 'counts all models the normal way' do
- expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ context '.approximate_counts' do
+ context 'selecting strategies' do
+ let(:strategies) { [double('s1', enabled?: true), double('s2', enabled?: false)] }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
+ it 'uses only enabled strategies' do
+ expect(strategies[0]).to receive(:new).and_return(double('strategy1', count: {}))
+ expect(strategies[1]).not_to receive(:new)
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ described_class.approximate_counts(models, strategies: strategies)
end
end
- context 'with PostgreSQL', :postgresql do
- describe 'when reltuples have not been updated' do
- it 'counts all models the normal way' do
- expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({})
+ context 'fallbacks' do
+ subject { described_class.approximate_counts(models, strategies: strategies) }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ let(:strategies) do
+ [
+ double('s1', enabled?: true, new: first_strategy),
+ double('s2', enabled?: true, new: second_strategy)
+ ]
end
- describe 'no permission' do
- it 'falls back to standard query' do
- allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege)
+ let(:first_strategy) { double('first strategy', count: {}) }
+ let(:second_strategy) { double('second strategy', count: {}) }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ it 'gets results from first strategy' do
+ expect(strategies[0]).to receive(:new).with(models).and_return(first_strategy)
+ expect(first_strategy).to receive(:count)
+
+ subject
end
- describe 'when some reltuples have been updated' do
- it 'counts projects in the fast way' do
- expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 })
+ it 'gets more results from second strategy if some counts are missing' do
+ expect(first_strategy).to receive(:count).and_return({ Project => 3 })
+ expect(strategies[1]).to receive(:new).with([Identity]).and_return(second_strategy)
+ expect(second_strategy).to receive(:count).and_return({ Identity => 1 })
- expect(Project).not_to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ expect(subject).to eq({ Project => 3, Identity => 1 })
end
- describe 'when all reltuples have been updated' do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
- ActiveRecord::Base.connection.execute('ANALYZE identities')
- end
-
- it 'counts models with the standard way' do
- expect(Project).not_to receive(:count)
- expect(Identity).not_to receive(:count)
+ it 'does not get more results as soon as all counts are present' do
+ expect(first_strategy).to receive(:count).and_return({ Project => 3, Identity => 1 })
+ expect(strategies[1]).not_to receive(:new)
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ subject
end
end
end
diff --git a/spec/lib/gitlab/diff/file_collection/commit_spec.rb b/spec/lib/gitlab/diff/file_collection/commit_spec.rb
index 6d1b66deb6a..34ed22b8941 100644
--- a/spec/lib/gitlab/diff/file_collection/commit_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/commit_spec.rb
@@ -12,4 +12,8 @@ describe Gitlab::Diff::FileCollection::Commit do
let(:diffable) { project.commit }
let(:stub_path) { 'bar/branch-test.txt' }
end
+
+ it_behaves_like 'unfoldable diff' do
+ let(:diffable) { project.commit }
+ end
end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index fbcf515281e..256166dbad3 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -2,22 +2,29 @@ require 'spec_helper'
describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:merge_request) { create(:merge_request) }
- let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files }
+ let(:subject) { described_class.new(merge_request.merge_request_diff, diff_options: nil) }
+ let(:diff_files) { subject.diff_files }
- it 'does not highlight binary files' do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(false)
+ describe '#diff_files' do
+ it 'does not highlight binary files' do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(false)
- expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
+ expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
- diff_files
- end
+ diff_files
+ end
+
+ it 'does not highlight files marked as undiffable in .gitattributes' do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(false)
- it 'does not highlight files marked as undiffable in .gitattributes' do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(false)
+ expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
- expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
+ diff_files
+ end
+ end
- diff_files
+ it_behaves_like 'unfoldable diff' do
+ let(:diffable) { merge_request.merge_request_diff }
end
it 'it uses a different cache key if diff line keys change' do
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/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index d8f01dcb76b..77f5b2ffa37 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -218,7 +218,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#fail_import' do
it 'marks the import as failed' do
- expect(project).to receive(:mark_import_as_failed).with('foo')
+ expect(project.import_state).to receive(:mark_as_failed).with('foo')
expect(importer.fail_import('foo')).to eq(false)
end
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
index 20b48c1de68..f5df38c9aaf 100644
--- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -36,7 +36,7 @@ describe Gitlab::GithubImport::ParallelImporter do
it 'updates the import JID of the project' do
importer.execute
- expect(project.reload.import_jid).to eq("github-importer/#{project.id}")
+ expect(project.import_state.reload.jid).to eq("github-importer/#{project.id}")
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/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 30686634af4..f3de7adcec7 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -34,6 +34,28 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect { relation.update_all(share_with_group_lock: false) }
.to raise_error(ActiveRecord::ReadOnlyRecord)
end
+
+ describe 'hierarchy_order option' do
+ let(:relation) do
+ described_class.new(Group.where(id: child2.id)).base_and_ancestors(hierarchy_order: hierarchy_order)
+ end
+
+ context ':asc' do
+ let(:hierarchy_order) { :asc }
+
+ it 'orders by child to parent' do
+ expect(relation).to eq([child2, child1, parent])
+ end
+ end
+
+ context ':desc' do
+ let(:hierarchy_order) { :desc }
+
+ it 'orders by parent to child' do
+ expect(relation).to eq([parent, child1, child2])
+ end
+ end
+ end
end
describe '#base_and_descendants' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 1d184375a52..bae5b21c26f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -94,6 +94,7 @@ merge_requests:
- timelogs
- head_pipeline
- latest_merge_request_diff
+- merge_request_pipelines
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -102,7 +103,7 @@ merge_request_diff_commits:
- merge_request_diff
merge_request_diff_files:
- merge_request_diff
-pipelines:
+ci_pipelines:
- project
- user
- stages
@@ -121,6 +122,9 @@ pipelines:
- artifacts
- pipeline_schedule
- merge_requests
+- merge_request
+- deployments
+- environments
pipeline_variables:
- pipeline
stages:
@@ -245,6 +249,7 @@ project:
- protected_branches
- protected_tags
- project_members
+- project_repository
- users
- requesters
- deploy_keys_projects
@@ -260,7 +265,8 @@ project:
- notification_settings
- import_data
- commit_statuses
-- pipelines
+- ci_pipelines
+- all_pipelines
- stages
- builds
- runner_projects
@@ -281,6 +287,7 @@ project:
- statistics
- container_repositories
- uploads
+- file_uploads
- import_state
- members_and_requesters
- build_trace_section_names
@@ -301,6 +308,7 @@ project:
- import_export_upload
- repository_languages
- pool_repository
+- kubernetes_namespaces
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 7171e12a849..242c16c4bdc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -197,9 +197,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'has the correct number of pipelines and statuses' do
- expect(@project.pipelines.size).to eq(5)
+ expect(@project.ci_pipelines.size).to eq(5)
- @project.pipelines.zip([2, 2, 2, 2, 2])
+ @project.ci_pipelines.zip([2, 2, 2, 2, 2])
.each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size)
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 5dc372263ad..46fdfba953b 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -119,16 +119,16 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end
it 'has pipeline stages' do
- expect(saved_project_json.dig('pipelines', 0, 'stages')).not_to be_empty
+ expect(saved_project_json.dig('ci_pipelines', 0, 'stages')).not_to be_empty
end
it 'has pipeline statuses' do
- expect(saved_project_json.dig('pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
+ expect(saved_project_json.dig('ci_pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
end
it 'has pipeline builds' do
builds_count = saved_project_json
- .dig('pipelines', 0, 'stages', 0, 'statuses')
+ .dig('ci_pipelines', 0, 'stages', 0, 'statuses')
.count { |hash| hash['type'] == 'Ci::Build' }
expect(builds_count).to eq(1)
@@ -142,11 +142,11 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end
it 'has pipeline commits' do
- expect(saved_project_json['pipelines']).not_to be_empty
+ expect(saved_project_json['ci_pipelines']).not_to be_empty
end
it 'has ci pipeline notes' do
- expect(saved_project_json['pipelines'].first['notes']).not_to be_empty
+ expect(saved_project_json['ci_pipelines'].first['notes']).not_to be_empty
end
it 'has labels with no associations' do
diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
new file mode 100644
index 00000000000..a20a844a492
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationRenameService do
+ let(:renames) do
+ {
+ 'example_relation1' => 'new_example_relation1',
+ 'example_relation2' => 'new_example_relation2'
+ }
+ end
+
+ let(:user) { create(:admin) }
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:shared) { project.import_export_shared }
+
+ before do
+ stub_const("#{described_class}::RENAMES", renames)
+ end
+
+ context 'when importing' do
+ let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) }
+ let(:import_path) { 'spec/lib/gitlab/import_export' }
+ let(:file_content) { IO.read("#{import_path}/project.json") }
+ let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
+ let(:tree_hash) { project_tree_restorer.instance_variable_get(:@tree_hash) }
+
+ before do
+ allow(shared).to receive(:export_path).and_return(import_path)
+ allow(ActiveSupport::JSON).to receive(:decode).with(file_content).and_return(json_file)
+ end
+
+ context 'when the file has only old relationship names' do
+ # Configuring the json as an old version exported file, with only
+ # the previous association with the old name
+ before do
+ renames.each do |old_name, _|
+ json_file[old_name.to_s] = []
+ end
+ end
+
+ it 'renames old relationships to the new name' do
+ expect(json_file.keys).to include(*renames.keys)
+
+ project_tree_restorer.restore
+
+ expect(json_file.keys).to include(*renames.values)
+ expect(json_file.keys).not_to include(*renames.keys)
+ end
+ end
+
+ context 'when the file has both the old and new relationships' do
+ # Configuring the json as the new version exported file, with both
+ # the old association name and the new one
+ before do
+ renames.each do |old_name, new_name|
+ json_file[old_name.to_s] = [1]
+ json_file[new_name.to_s] = [2]
+ end
+ end
+
+ it 'uses the new relationships and removes the old ones from the hash' do
+ expect(json_file.keys).to include(*renames.keys)
+
+ project_tree_restorer.restore
+
+ expect(json_file.keys).to include(*renames.values)
+ expect(json_file.values_at(*renames.values).flatten.uniq.first).to eq 2
+ expect(json_file.keys).not_to include(*renames.keys)
+ end
+ end
+
+ context 'when the file has only new relationship names' do
+ # Configuring the json as the future version exported file, with only
+ # the new association name
+ before do
+ renames.each do |_, new_name|
+ json_file[new_name.to_s] = []
+ end
+ end
+
+ it 'uses the new relationships' do
+ expect(json_file.keys).not_to include(*renames.keys)
+
+ project_tree_restorer.restore
+
+ expect(json_file.keys).to include(*renames.values)
+ end
+ end
+ end
+
+ context 'when exporting' do
+ let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: user, shared: shared) }
+ let(:project_tree) { project_tree_saver.send(:project_json) }
+
+ it 'adds old relationships to the exported file' do
+ project_tree.merge!(renames.values.map { |new_name| [new_name, []] }.to_h)
+
+ allow(project_tree_saver).to receive(:save) do |arg|
+ project_tree_saver.send(:project_json_tree)
+ end
+
+ result = project_tree_saver.save
+
+ saved_data = ActiveSupport::JSON.decode(result)
+
+ expect(saved_data.keys).to include(*(renames.keys + renames.values))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index f7935149b23..d3bfde181bc 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -243,6 +243,7 @@ Ci::Pipeline:
- failure_reason
- protected
- iid
+- merge_request_id
Ci::Stage:
- id
- name
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/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 3979a43216c..8fc85301304 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -99,6 +99,7 @@ describe Gitlab::Kubernetes::KubeClient do
:create_secret,
:create_service_account,
:update_config_map,
+ :update_secret,
:update_service_account
].each do |method|
describe "##{method}" do
@@ -174,6 +175,84 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ shared_examples 'create_or_update method' do
+ let(:get_method) { "get_#{resource_type}" }
+ let(:update_method) { "update_#{resource_type}" }
+ let(:create_method) { "create_#{resource_type}" }
+
+ context 'resource exists' do
+ before do
+ expect(client).to receive(get_method).and_return(resource)
+ end
+
+ it 'calls the update method' do
+ expect(client).to receive(update_method).with(resource)
+
+ subject
+ end
+ end
+
+ context 'resource does not exist' do
+ before do
+ expect(client).to receive(get_method).and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
+ end
+
+ it 'calls the create method' do
+ expect(client).to receive(create_method).with(resource)
+
+ subject
+ end
+ end
+ end
+
+ describe '#create_or_update_cluster_role_binding' do
+ let(:resource_type) { 'cluster_role_binding' }
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
+ end
+
+ subject { client.create_or_update_cluster_role_binding(resource) }
+
+ it_behaves_like 'create_or_update method'
+ end
+
+ describe '#create_or_update_role_binding' do
+ let(:resource_type) { 'role_binding' }
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
+ end
+
+ subject { client.create_or_update_role_binding(resource) }
+
+ it_behaves_like 'create_or_update method'
+ end
+
+ describe '#create_or_update_service_account' do
+ let(:resource_type) { 'service_account' }
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
+ end
+
+ subject { client.create_or_update_service_account(resource) }
+
+ it_behaves_like 'create_or_update method'
+ end
+
+ describe '#create_or_update_secret' do
+ let(:resource_type) { 'secret' }
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(metadata: { name: 'name', namespace: 'namespace' })
+ end
+
+ subject { client.create_or_update_secret(resource) }
+
+ it_behaves_like 'create_or_update method'
+ end
+
describe 'methods that do not exist on any client' do
it 'throws an error' do
expect { client.non_existent_method }.to raise_error(NoMethodError)
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index 5c03a2ce7d3..f326d57e9c6 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -48,26 +48,30 @@ describe Gitlab::Kubernetes do
end
describe '#to_kubeconfig' do
+ let(:token) { 'TOKEN' }
+ let(:ca_pem) { 'PEM' }
+
subject do
to_kubeconfig(
url: 'https://kube.domain.com',
namespace: 'NAMESPACE',
- token: 'TOKEN',
- ca_pem: ca_pem)
+ token: token,
+ ca_pem: ca_pem
+ )
end
- context 'when CA PEM is provided' do
- let(:ca_pem) { 'PEM' }
- let(:path) { expand_fixture_path('config/kubeconfig.yml') }
-
- it { is_expected.to eq(YAML.load_file(path)) }
- end
+ it { expect(YAML.safe_load(subject)).to eq(YAML.load_file(expand_fixture_path('config/kubeconfig.yml'))) }
context 'when CA PEM is not provided' do
let(:ca_pem) { nil }
- let(:path) { expand_fixture_path('config/kubeconfig-without-ca.yml') }
- it { is_expected.to eq(YAML.load_file(path)) }
+ it { expect(YAML.safe_load(subject)).to eq(YAML.load_file(expand_fixture_path('config/kubeconfig-without-ca.yml'))) }
+ end
+
+ context 'when token is not provided' do
+ let(:token) { nil }
+
+ it { is_expected.to be_nil }
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index 20514486727..d2df21d7bb5 100644
--- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -174,7 +174,7 @@ describe Gitlab::LegacyGithubImport::Importer do
described_class.new(project).execute
- expect(project.import_error).to eq error.to_json
+ expect(project.import_state.last_error).to eq error.to_json
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 3a20dad16d0..77ee30264bf 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -48,4 +48,59 @@ describe Gitlab::LfsToken do
end
end
end
+
+ describe '#deploy_key_pushable?' do
+ let(:lfs_token) { described_class.new(actor) }
+
+ context 'when actor is not a DeployKey' do
+ let(:actor) { create(:user) }
+ let(:project) { create(:project) }
+
+ it 'returns false' do
+ expect(lfs_token.deploy_key_pushable?(project)).to be_falsey
+ end
+ end
+
+ context 'when actor is a DeployKey' do
+ let(:deploy_keys_project) { create(:deploy_keys_project, can_push: can_push) }
+ let(:project) { deploy_keys_project.project }
+ let(:actor) { deploy_keys_project.deploy_key }
+
+ context 'but the DeployKey cannot push to the project' do
+ let(:can_push) { false }
+
+ it 'returns false' do
+ expect(lfs_token.deploy_key_pushable?(project)).to be_falsey
+ end
+ end
+
+ context 'and the DeployKey can push to the project' do
+ let(:can_push) { true }
+
+ it 'returns true' do
+ expect(lfs_token.deploy_key_pushable?(project)).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '#type' do
+ let(:lfs_token) { described_class.new(actor) }
+
+ context 'when actor is not a User' do
+ let(:actor) { create(:deploy_key) }
+
+ it 'returns false' do
+ expect(lfs_token.type).to eq(:lfs_deploy_token)
+ end
+ end
+
+ context 'when actor is a User' do
+ let(:actor) { create(:user) }
+
+ it 'returns false' do
+ expect(lfs_token.type).to eq(:lfs_token)
+ end
+ 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 8df0facdab3..39e0a17a307 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -10,8 +10,8 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?(import_url)).to be false
end
- it 'allows imports from configured SSH host and port' do
- import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
+ it 'allows mirroring from configured SSH host and port' do
+ import_url = "ssh://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
expect(described_class.blocked_url?(import_url)).to be false
end
@@ -29,24 +29,46 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
+ it 'returns true for bad protocol on configured web/SSH host and ports' do
+ web_url = "javascript://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git%0aalert(1)"
+ expect(described_class.blocked_url?(web_url)).to be true
+
+ ssh_url = "javascript://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git%0aalert(1)"
+ expect(described_class.blocked_url?(ssh_url)).to be true
+ end
+
it 'returns true for localhost IPs' do
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:0:0:0]/foo/foo.git')).to be true
expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
- expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::]/foo/foo.git')).to be true
end
it 'returns true for loopback IP' do
expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
end
+ it 'returns true for alternative version of 127.0.0.1 (017700000001)' do
+ expect(described_class.blocked_url?('https://017700000001:65535/foo/foo.git')).to be true
+ end
+
it 'returns true for alternative version of 127.0.0.1 (0x7f.1)' do
expect(described_class.blocked_url?('https://0x7f.1:65535/foo/foo.git')).to be true
end
+ it 'returns true for alternative version of 127.0.0.1 (0x7f.0.0.1)' do
+ expect(described_class.blocked_url?('https://0x7f.0.0.1:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for alternative version of 127.0.0.1 (0x7f000001)' do
+ expect(described_class.blocked_url?('https://0x7f000001:65535/foo/foo.git')).to be true
+ end
+
it 'returns true for alternative version of 127.0.0.1 (2130706433)' do
expect(described_class.blocked_url?('https://2130706433:65535/foo/foo.git')).to be true
end
@@ -55,6 +77,27 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://127.000.000.001:65535/foo/foo.git')).to be true
end
+ it 'returns true for alternative version of 127.0.0.1 (127.0.1)' do
+ expect(described_class.blocked_url?('https://127.0.1:65535/foo/foo.git')).to be true
+ end
+
+ context 'with ipv6 mapped address' do
+ it 'returns true for localhost IPs' do
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:0.0.0.0]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:0.0.0.0]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:0:0]/foo/foo.git')).to be true
+ end
+
+ it 'returns true for loopback IPs' do
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:127.0.0.1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:7f00:1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[0:0:0:0:0:ffff:127.0.0.2]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:127.0.0.2]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::ffff:7f00:2]/foo/foo.git')).to be true
+ end
+ end
+
it 'returns true for a non-alphanumeric hostname' do
stub_resolv
@@ -78,7 +121,22 @@ describe Gitlab::UrlBlocker do
end
context 'when allow_local_network is' do
- let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
+ let(:local_ips) do
+ [
+ '192.168.1.2',
+ '[0:0:0:0:0:ffff:192.168.1.2]',
+ '[::ffff:c0a8:102]',
+ '10.0.0.2',
+ '[0:0:0:0:0:ffff:10.0.0.2]',
+ '[::ffff:a00:2]',
+ '172.16.0.2',
+ '[0:0:0:0:0:ffff:172.16.0.2]',
+ '[::ffff:ac10:20]',
+ '[feef::1]',
+ '[fee2::]',
+ '[fc00:bf8b:e62c:abcd:abcd:aaaa:aaaa:aaaa]'
+ ]
+ end
let(:fake_domain) { 'www.fakedomain.fake' }
context 'true (default)' do
@@ -109,10 +167,14 @@ describe Gitlab::UrlBlocker do
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
end
- # This is blocked due to the hostname check: https://gitlab.com/gitlab-org/gitlab-ce/issues/50227
- it 'blocks IPv6 link-local endpoints' do
- expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]')
- expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]')
+ it 'allows IPv6 link-local endpoints' do
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]')
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.169.254]')
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a9fe]')
+ expect(described_class).not_to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]')
+ expect(described_class).not_to be_blocked_url('http://[::ffff:169.254.168.100]')
+ expect(described_class).not_to be_blocked_url('http://[::ffff:a9fe:a864]')
+ expect(described_class).not_to be_blocked_url('http://[fe80::c800:eff:fe74:8]')
end
end
@@ -135,14 +197,20 @@ describe Gitlab::UrlBlocker do
end
it 'blocks IPv6 link-local endpoints' do
+ expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.169.254]', allow_local_network: false)
expect(described_class).to be_blocked_url('http://[::ffff:169.254.169.254]', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a9fe]', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://[0:0:0:0:0:ffff:169.254.168.100]', allow_local_network: false)
expect(described_class).to be_blocked_url('http://[::ffff:169.254.168.100]', allow_local_network: false)
- expect(described_class).to be_blocked_url('http://[FE80::C800:EFF:FE74:8]', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://[::ffff:a9fe:a864]', allow_local_network: false)
+ expect(described_class).to be_blocked_url('http://[fe80::c800:eff:fe74:8]', allow_local_network: false)
end
end
def stub_domain_resolv(domain, ip)
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)])
+ address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
+ allow(address).to receive(:ipv6_v4mapped?).and_return(false)
end
def unstub_domain_resolv
@@ -183,6 +251,36 @@ describe Gitlab::UrlBlocker do
end
end
+ describe '#validate_hostname!' do
+ let(:ip_addresses) do
+ [
+ '2001:db8:1f70::999:de8:7648:6e8',
+ 'FE80::C800:EFF:FE74:8',
+ '::ffff:127.0.0.1',
+ '::ffff:169.254.168.100',
+ '::ffff:7f00:1',
+ '0:0:0:0:0:ffff:0.0.0.0',
+ 'localhost',
+ '127.0.0.1',
+ '127.000.000.001',
+ '0x7f000001',
+ '0x7f.0.0.1',
+ '0x7f.0.0.1',
+ '017700000001',
+ '0177.1',
+ '2130706433',
+ '::',
+ '::1'
+ ]
+ end
+
+ it 'does not raise error for valid Ip addresses' do
+ ip_addresses.each do |ip|
+ expect { described_class.send(:validate_hostname!, ip) }.not_to raise_error
+ end
+ end
+ end
+
# Resolv does not support resolving UTF-8 domain names
# See https://bugs.ruby-lang.org/issues/4270
def stub_resolv
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 5390f237073..deb19fe1a4b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -17,6 +17,9 @@ describe Gitlab::UsageData do
gcp_cluster = create(:cluster, :provided_by_gcp)
create(:cluster, :provided_by_user)
create(:cluster, :provided_by_user, :disabled)
+ create(:cluster, :group)
+ create(:cluster, :group, :disabled)
+ create(:cluster, :group, :disabled)
create(:clusters_applications_helm, :installed, cluster: gcp_cluster)
create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
create(:clusters_applications_cert_managers, :installed, cluster: gcp_cluster)
@@ -77,7 +80,11 @@ describe Gitlab::UsageData do
environments
clusters
clusters_enabled
+ project_clusters_enabled
+ group_clusters_enabled
clusters_disabled
+ project_clusters_disabled
+ group_clusters_disabled
clusters_platforms_gke
clusters_platforms_user
clusters_applications_helm
@@ -127,8 +134,13 @@ describe Gitlab::UsageData do
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
- expect(count_data[:clusters_enabled]).to eq(6)
- expect(count_data[:clusters_disabled]).to eq(1)
+ expect(count_data[:clusters_enabled]).to eq(7)
+ expect(count_data[:project_clusters_enabled]).to eq(6)
+ expect(count_data[:group_clusters_enabled]).to eq(1)
+ expect(count_data[:clusters_disabled]).to eq(3)
+ expect(count_data[:project_clusters_disabled]).to eq(1)
+ expect(count_data[:group_clusters_disabled]).to eq(2)
+ expect(count_data[:group_clusters_enabled]).to eq(1)
expect(count_data[:clusters_platforms_gke]).to eq(1)
expect(count_data[:clusters_platforms_user]).to eq(1)
expect(count_data[:clusters_applications_helm]).to eq(1)
@@ -201,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 ad2c9d7f2af..47a5fd0bdb4 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
{
@@ -127,4 +153,42 @@ describe Gitlab::Utils do
end
end
end
+
+ describe '.ensure_utf8_size' do
+ context 'string is has less bytes than expected' do
+ it 'backfills string with null characters' do
+ transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32)
+
+ expect(transformed.bytesize).to eq 32
+ expect(transformed).to eq(('a' * 10) + ('0' * 22))
+ end
+ end
+
+ context 'string size is exactly the one that is expected' do
+ it 'returns original value' do
+ transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32)
+
+ expect(transformed).to eq 'a' * 32
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+
+ context 'when string contains a few multi-byte UTF characters' do
+ it 'backfills string with null characters' do
+ transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32)
+
+ expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14)
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+
+ context 'when string has multiple multi-byte UTF chars exceeding 32 bytes' do
+ it 'truncates string to 32 characters and backfills it if needed' do
+ transformed = described_class.ensure_utf8_size('❤' * 18, bytes: 32)
+
+ expect(transformed).to eq(('❤' * 10) + ('0' * 2))
+ expect(transformed.bytesize).to eq 32
+ end
+ end
+ end
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 ff1a5aa2536..150c00e4bfe 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -522,7 +522,7 @@ describe Notify do
let(:project_snippet) { create(:project_snippet, project: project) }
let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
- subject { described_class.note_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
+ subject { described_class.note_project_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { project_snippet }
diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
new file mode 100644
index 00000000000..34f4a36d63d
--- /dev/null
+++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20181010133639_backfill_store_project_full_path_in_repo.rb')
+
+describe BackfillStoreProjectFullPathInRepo, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:group) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:subgroup) { namespaces.create!(name: 'bar', path: 'bar', parent_id: group.id) }
+
+ subject(:migration) { described_class.new }
+
+ around do |example|
+ Sidekiq::Testing.inline! do
+ example.run
+ end
+ end
+
+ describe '#up' do
+ shared_examples_for 'writes the full path to git config' do
+ it 'writes the git config' do
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ allow(repository_service).to receive(:cleanup)
+ expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => expected_path)
+ end
+
+ migration.up
+ end
+
+ it 'retries in case of failure' do
+ repository_service = spy(:repository_service)
+
+ allow(Gitlab::GitalyClient::RepositoryService).to receive(:new).and_return(repository_service)
+
+ allow(repository_service).to receive(:set_config).and_raise(GRPC::BadStatus, 'Retry me')
+ expect(repository_service).to receive(:set_config).exactly(3).times
+
+ migration.up
+ end
+
+ it 'cleans up repository before writing the config' do
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
+ expect(repository_service).to receive(:cleanup).ordered
+ expect(repository_service).to receive(:set_config).ordered
+ end
+
+ migration.up
+ end
+
+ context 'legacy storage' do
+ it 'finds the repository at the correct location' do
+ Project.find(project.id).create_repository
+
+ expect { migration.up }.not_to raise_error
+ end
+ end
+
+ context 'hashed storage' do
+ it 'finds the repository at the correct location' do
+ project.update_attribute(:storage_version, 1)
+
+ Project.find(project.id).create_repository
+
+ expect { migration.up }.not_to raise_error
+ end
+ end
+ end
+
+ context 'project in group' do
+ let!(:project) { projects.create!(namespace_id: group.id, name: 'baz', path: 'baz') }
+ let(:expected_path) { 'foo/baz' }
+
+ it_behaves_like 'writes the full path to git config'
+ end
+
+ context 'project in subgroup' do
+ let!(:project) { projects.create!(namespace_id: subgroup.id, name: 'baz', path: 'baz') }
+ let(:expected_path) { 'foo/bar/baz' }
+
+ it_behaves_like 'writes the full path to git config'
+ end
+ end
+
+ describe '#down' do
+ context 'project in group' do
+ let!(:project) { projects.create!(namespace_id: group.id, name: 'baz', path: 'baz') }
+
+ it 'deletes the gitlab full config value' do
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
+ .to receive(:delete_config).with(['gitlab.fullpath'])
+
+ migration.down
+ end
+ end
+ end
+end
diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb
index 0258860d169..7876536cb3e 100644
--- a/spec/migrations/clean_up_for_members_spec.rb
+++ b/spec/migrations/clean_up_for_members_spec.rb
@@ -3,6 +3,7 @@ require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb
describe CleanUpForMembers, :migration do
let(:migration) { described_class.new }
+ let(:groups) { table(:namespaces) }
let!(:group_member) { create_group_member }
let!(:unbinded_group_member) { create_group_member }
let!(:invited_group_member) { create_group_member(true) }
@@ -25,7 +26,7 @@ describe CleanUpForMembers, :migration do
end
def create_group_member(invited = false)
- fill_member(GroupMember.new(group: create_group), invited)
+ fill_member(GroupMember.new(source_id: create_group.id, source_type: 'Namespace'), invited)
end
def create_project_member(invited = false)
@@ -54,7 +55,7 @@ describe CleanUpForMembers, :migration do
def create_group
name = FFaker::Lorem.characters(10)
- Group.create(name: name, path: name.downcase.gsub(/\s/, '_'))
+ groups.create!(type: 'Group', name: name, path: name.downcase.gsub(/\s/, '_'))
end
def create_project
diff --git a/spec/migrations/cleanup_environments_external_url_spec.rb b/spec/migrations/cleanup_environments_external_url_spec.rb
new file mode 100644
index 00000000000..07ddaf3d38f
--- /dev/null
+++ b/spec/migrations/cleanup_environments_external_url_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20181108091549_cleanup_environments_external_url.rb')
+
+describe CleanupEnvironmentsExternalUrl, :migration do
+ let(:environments) { table(:environments) }
+ let(:invalid_entries) { environments.where(environments.arel_table[:external_url].matches('javascript://%')) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ before do
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+
+ environments.create!(id: 1, project_id: project.id, name: 'poisoned', slug: 'poisoned', external_url: 'javascript://alert("1")')
+ end
+
+ it 'clears every environment with a javascript external_url' do
+ expect do
+ subject.up
+ end.to change { invalid_entries.count }.from(1).to(0)
+ end
+
+ it 'do not removes environments' do
+ expect do
+ subject.up
+ end.not_to change { environments.count }
+ end
+end
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
index 4af51217031..8c55daf0d37 100644
--- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -94,17 +94,18 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
context 'for milestones (by group)' do
- # milestones (by group) is a little different than all of the other models
- let!(:group1) { create(:group) }
- let!(:group2) { create(:group) }
- let!(:group3) { create(:group) }
+ # milestones (by group) is a little different than most of the other models
+ let(:groups) { table(:namespaces) }
+ let(:group1) { groups.create(name: 'Group 1', type: 'Group', path: 'group_1') }
+ let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') }
+ let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') }
let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } }
before do
- 3.times { create(:milestone, group: group1) }
- 3.times { create(:milestone, group: group2) }
- 3.times { create(:milestone, group: group3) }
+ 3.times { create(:milestone, group_id: group1.id) }
+ 3.times { create(:milestone, group_id: group2.id) }
+ 3.times { create(:milestone, group_id: group3.id) }
internal_id_query.call(group1).first.tap do |iid|
iid.last_value = iid.last_value - 2
diff --git a/spec/migrations/migrate_forbidden_redirect_uris_spec.rb b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb
new file mode 100644
index 00000000000..0bc13a3974a
--- /dev/null
+++ b/spec/migrations/migrate_forbidden_redirect_uris_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181026091631_migrate_forbidden_redirect_uris.rb')
+
+describe MigrateForbiddenRedirectUris, :migration do
+ let(:oauth_application) { table(:oauth_applications) }
+ let(:oauth_access_grant) { table(:oauth_access_grants) }
+
+ let!(:control_app) { oauth_application.create(random_params) }
+ let!(:control_access_grant) { oauth_application.create(random_params) }
+ let!(:forbidden_js_app) { oauth_application.create(random_params.merge(redirect_uri: 'javascript://alert()')) }
+ let!(:forbidden_vb_app) { oauth_application.create(random_params.merge(redirect_uri: 'VBSCRIPT://alert()')) }
+ let!(:forbidden_access_grant) { oauth_application.create(random_params.merge(redirect_uri: 'vbscript://alert()')) }
+
+ context 'oauth application' do
+ it 'migrates forbidden javascript URI' do
+ expect { migrate! }.to change { forbidden_js_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten')
+ end
+
+ it 'migrates forbidden VBScript URI' do
+ expect { migrate! }.to change { forbidden_vb_app.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten')
+ end
+
+ it 'does not migrate a valid URI' do
+ expect { migrate! }.not_to change { control_app.reload.redirect_uri }
+ end
+ end
+
+ context 'access grant' do
+ it 'migrates forbidden VBScript URI' do
+ expect { migrate! }.to change { forbidden_access_grant.reload.redirect_uri }.to('http://forbidden-scheme-has-been-overwritten')
+ end
+
+ it 'does not migrate a valid URI' do
+ expect { migrate! }.not_to change { control_access_grant.reload.redirect_uri }
+ end
+ end
+
+ def random_params
+ {
+ name: 'test',
+ secret: 'test',
+ uid: Doorkeeper::OAuth::Helpers::UniqueToken.generate,
+ redirect_uri: 'http://valid.com'
+ }
+ end
+end
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
index 9220b49a736..0016f058a17 100644
--- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -18,33 +18,33 @@ describe MigrateIssuesToGhostUser, :migration do
let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') }
it 'does not create a new user' do
- expect { schema_migrate_up! }.not_to change { User.count }
+ expect { migrate! }.not_to change { User.count }
end
it 'migrates issues where author = nil to the ghost user' do
- schema_migrate_up!
+ migrate!
expect(issues.first.reload.author_id).to eq(ghost.id)
end
it 'does not change issues authored by an existing user' do
- expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ expect { migrate! }.not_to change { issues.second.reload.author_id}
end
end
context 'when ghost user does not exist' do
it 'creates a new user' do
- expect { schema_migrate_up! }.to change { User.count }.by(1)
+ expect { migrate! }.to change { User.count }.by(1)
end
it 'migrates issues where author = nil to the ghost user' do
- schema_migrate_up!
+ migrate!
expect(issues.first.reload.author_id).to eq(User.ghost.id)
end
it 'does not change issues authored by an existing user' do
- expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ expect { migrate! }.not_to change { issues.second.reload.author_id}
end
end
end
diff --git a/spec/migrations/schedule_runners_token_encryption_spec.rb b/spec/migrations/schedule_runners_token_encryption_spec.rb
new file mode 100644
index 00000000000..376d2795277
--- /dev/null
+++ b/spec/migrations/schedule_runners_token_encryption_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181121111200_schedule_runners_token_encryption')
+
+describe ScheduleRunnersTokenEncryption, :migration do
+ let(:settings) { table(:application_settings) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:runners) { table(:ci_runners) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ settings.create!(id: 1, runners_registration_token: 'plain-text-token1')
+ namespaces.create!(id: 11, name: 'gitlab', path: 'gitlab-org', runners_token: 'my-token1')
+ namespaces.create!(id: 12, name: 'gitlab', path: 'gitlab-org', runners_token: 'my-token2')
+ projects.create!(id: 111, namespace_id: 11, name: 'gitlab', path: 'gitlab-ce', runners_token: 'my-token1')
+ projects.create!(id: 114, namespace_id: 11, name: 'gitlab', path: 'gitlab-ce', runners_token: 'my-token2')
+ runners.create!(id: 201, runner_type: 1, token: 'plain-text-token1')
+ runners.create!(id: 202, runner_type: 1, token: 'plain-text-token2')
+ end
+
+ it 'schedules runners token encryption migration for multiple resources' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 'settings', 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 'namespace', 11, 11)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, 'namespace', 12, 12)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 'project', 111, 111)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, 'project', 114, 114)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 'runner', 201, 201)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, 'runner', 202, 202)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 7
+ 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/blob_spec.rb b/spec/models/blob_spec.rb
index ed93f94d893..e8c03b587e2 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -18,6 +18,7 @@ describe Blob do
describe '.lazy' do
let(:project) { create(:project, :repository) }
+ let(:same_project) { Project.find(project.id) }
let(:other_project) { create(:project, :repository) }
let(:commit_id) { 'e63f41fe459e62e1228fcef60d7189127aeba95a' }
@@ -32,7 +33,7 @@ describe Blob do
expect(other_project.repository).not_to receive(:blobs_at)
changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
- contributing = described_class.lazy(project, commit_id, 'CONTRIBUTING.md')
+ contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
described_class.lazy(other_project, commit_id, 'CHANGELOG')
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 6dba132184c..519968b9e48 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -15,6 +15,8 @@ describe Ci::BuildMetadata do
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:build_metadata) { build.metadata }
+ it_behaves_like 'having unique enum values'
+
describe '#update_timeout_state' do
subject { build_metadata }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index d02c3a5765f..89f78f629d4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -769,33 +769,15 @@ describe Ci::Build do
let(:subject) { build.hide_secrets(data) }
context 'hide runners token' do
- let(:data) { 'new token data'}
+ let(:data) { "new #{project.runners_token} data"}
- before do
- build.project.update(runners_token: 'token')
- end
-
- it { is_expected.to eq('new xxxxx data') }
+ it { is_expected.to match(/^new x+ data$/) }
end
context 'hide build token' do
- let(:data) { 'new token data'}
-
- before do
- build.update(token: 'token')
- end
-
- it { is_expected.to eq('new xxxxx data') }
- end
-
- context 'hide build token' do
- let(:data) { 'new token data'}
-
- before do
- build.update(token: 'token')
- end
+ let(:data) { "new #{build.token} data"}
- it { is_expected.to eq('new xxxxx data') }
+ it { is_expected.to match(/^new x+ data$/) }
end
end
@@ -1943,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}
@@ -2159,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/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 59c861a74db..d214fdf369a 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -12,6 +12,8 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data)
end
+ it_behaves_like 'having unique enum values'
+
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_artifacts_object_storage
@@ -436,32 +438,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
context 'when data exists' do
- let(:data) { 'Sample data in redis' }
-
before do
build_trace_chunk.send(:unsafe_set_data!, data)
end
- it 'persists the data' do
- expect(build_trace_chunk.redis?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ context 'when data size reached CHUNK_SIZE' do
+ let(:data) { 'a' * described_class::CHUNK_SIZE }
- subject
+ it 'persists the data' do
+ expect(build_trace_chunk.redis?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
- expect(build_trace_chunk.fog?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ it_behaves_like 'Atomic operation'
end
- it_behaves_like 'Atomic operation'
+ context 'when data size has not reached CHUNK_SIZE' do
+ let(:data) { 'Sample data in redis' }
+
+ it 'does not persist the data and the orignal data is intact' do
+ expect { subject }.to raise_error(described_class::FailedToPersistDataError)
+
+ expect(build_trace_chunk.redis?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
end
context 'when data does not exist' do
it 'does not persist' do
- expect { subject }.to raise_error('Can not persist empty data')
+ expect { subject }.to raise_error(described_class::FailedToPersistDataError)
end
end
end
@@ -470,32 +487,47 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :database }
context 'when data exists' do
- let(:data) { 'Sample data in database' }
-
before do
build_trace_chunk.send(:unsafe_set_data!, data)
end
- it 'persists the data' do
- expect(build_trace_chunk.database?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
- expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ context 'when data size reached CHUNK_SIZE' do
+ let(:data) { 'a' * described_class::CHUNK_SIZE }
- subject
+ it 'persists the data' do
+ expect(build_trace_chunk.database?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
- expect(build_trace_chunk.fog?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ it_behaves_like 'Atomic operation'
end
- it_behaves_like 'Atomic operation'
+ context 'when data size has not reached CHUNK_SIZE' do
+ let(:data) { 'Sample data in database' }
+
+ it 'does not persist the data and the orignal data is intact' do
+ expect { subject }.to raise_error(described_class::FailedToPersistDataError)
+
+ expect(build_trace_chunk.database?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
end
context 'when data does not exist' do
it 'does not persist' do
- expect { subject }.to raise_error('Can not persist empty data')
+ expect { subject }.to raise_error(described_class::FailedToPersistDataError)
end
end
end
@@ -504,27 +536,37 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :fog }
context 'when data exists' do
- let(:data) { 'Sample data in fog' }
-
before do
build_trace_chunk.send(:unsafe_set_data!, data)
end
- it 'does not change data store' do
- expect(build_trace_chunk.fog?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ context 'when data size reached CHUNK_SIZE' do
+ let(:data) { 'a' * described_class::CHUNK_SIZE }
- subject
+ it 'does not change data store' do
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+
+ subject
- expect(build_trace_chunk.fog?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
- expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
+
+ it_behaves_like 'Atomic operation'
end
- it_behaves_like 'Atomic operation'
+ context 'when data size has not reached CHUNK_SIZE' do
+ let(:data) { 'Sample data in fog' }
+
+ it 'does not raise error' do
+ expect { subject }.not_to raise_error
+ end
+ end
end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index fb5bec4108a..c68ba02b8de 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -15,6 +15,8 @@ describe Ci::JobArtifact do
it { is_expected.to delegate_method(:open).to(:file) }
it { is_expected.to delegate_method(:exists?).to(:file) }
+ it_behaves_like 'having unique enum values'
+
describe '.test_reports' do
subject { described_class.test_reports }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 9e6146b8a44..b67c6a4cffa 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -8,10 +8,13 @@ describe Ci::Pipeline, :mailer do
create(:ci_empty_pipeline, status: :created, project: project)
end
+ it_behaves_like 'having unique enum values'
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to belong_to(:pipeline_schedule) }
+ it { is_expected.to belong_to(:merge_request) }
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
@@ -30,8 +33,131 @@ describe Ci::Pipeline, :mailer do
describe 'associations' do
it 'has a bidirectional relationship with projects' do
- expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:pipelines)
- expect(Project.reflect_on_association(:pipelines).has_inverse?).to eq(:project)
+ expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:all_pipelines)
+ expect(Project.reflect_on_association(:all_pipelines).has_inverse?).to eq(:project)
+ expect(Project.reflect_on_association(:ci_pipelines).has_inverse?).to eq(:project)
+ end
+ end
+
+ describe '.sort_by_merge_request_pipelines' do
+ subject { described_class.sort_by_merge_request_pipelines }
+
+ context 'when branch pipelines exist' do
+ let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) }
+ let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) }
+
+ it 'returns pipelines order by id' do
+ expect(subject).to eq([branch_pipeline_2,
+ branch_pipeline_1])
+ end
+ end
+
+ context 'when merge request pipelines exist' do
+ let!(:merge_request_pipeline_1) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ end
+
+ let!(:merge_request_pipeline_2) 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 'returns pipelines order by id' do
+ expect(subject).to eq([merge_request_pipeline_2,
+ merge_request_pipeline_1])
+ end
+ end
+
+ context 'when both branch pipeline and merge request pipeline exist' do
+ let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) }
+ let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) }
+
+ let!(:merge_request_pipeline_1) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ end
+
+ let!(:merge_request_pipeline_2) 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 'returns merge request pipeline first' do
+ expect(subject).to eq([merge_request_pipeline_2,
+ merge_request_pipeline_1,
+ branch_pipeline_2,
+ branch_pipeline_1])
+ end
+ end
+ end
+
+ describe '.merge_request' do
+ subject { described_class.merge_request }
+
+ context 'when there is a merge request pipeline' do
+ let!(:pipeline) { create(:ci_pipeline, source: :merge_request, merge_request: merge_request) }
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns merge request pipeline first' do
+ expect(subject).to eq([pipeline])
+ end
+ end
+
+ context 'when there are no merge request pipelines' do
+ let!(:pipeline) { create(:ci_pipeline, source: :push) }
+
+ it 'returns empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ describe 'Validations for merge request pipelines' do
+ let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) }
+
+ context 'when source is merge request' do
+ let(:source) { :merge_request }
+
+ context 'when merge request is specified' do
+ let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
+
+ it { expect(pipeline).to be_valid }
+ end
+
+ context 'when merge request is empty' do
+ let(:merge_request) { nil }
+
+ it { expect(pipeline).not_to be_valid }
+ end
+ end
+
+ context 'when source is web' do
+ let(:source) { :web }
+
+ context 'when merge request is specified' do
+ let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
+
+ it { expect(pipeline).not_to be_valid }
+ end
+
+ context 'when merge request is empty' do
+ let(:merge_request) { nil }
+
+ it { expect(pipeline).to be_valid }
+ end
end
end
@@ -224,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
@@ -758,27 +928,85 @@ describe Ci::Pipeline, :mailer do
describe '#branch?' do
subject { pipeline.branch? }
- context 'is not a tag' do
+ context 'when ref is not a tag' do
before do
pipeline.tag = false
end
- it 'return true when tag is set to false' do
+ it 'return true' do
is_expected.to be_truthy
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 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
end
- context 'is not a tag' do
+ context 'when ref is a tag' do
before do
pipeline.tag = true
end
- it 'return false when tag is set to true' do
+ it 'return false' do
is_expected.to be_falsey
end
end
end
+ describe '#git_ref' do
+ subject { pipeline.send(:git_ref) }
+
+ context 'when ref is branch' do
+ let(:pipeline) { create(:ci_pipeline, tag: false) }
+
+ it 'returns branch ref' do
+ is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s)
+ end
+ end
+
+ context 'when ref is tag' do
+ let(:pipeline) { create(:ci_pipeline, tag: true) }
+
+ it 'returns branch ref' do
+ is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.ref.to_s)
+ end
+ end
+
+ context 'when ref 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 'returns branch ref' do
+ is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s)
+ end
+ end
+ end
+
describe 'ref_exists?' do
context 'when repository exists' do
using RSpec::Parameterized::TableSyntax
@@ -1003,7 +1231,7 @@ describe Ci::Pipeline, :mailer do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')
- pipelines = project.pipelines.to_a
+ pipelines = project.ci_pipelines.to_a
pipelines.each(&:number_of_warnings)
@@ -1247,22 +1475,40 @@ describe Ci::Pipeline, :mailer do
describe '#ci_yaml_file_path' do
subject { pipeline.ci_yaml_file_path }
- it 'returns the path from project' do
- allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' }
+ %i[unknown_source repository_source].each do |source|
+ context source.to_s do
+ before do
+ pipeline.config_source = described_class.config_sources.fetch(source)
+ end
- is_expected.to eq('custom/path')
- end
+ it 'returns the path from project' do
+ allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' }
+
+ is_expected.to eq('custom/path')
+ end
+
+ it 'returns default when custom path is nil' do
+ allow(pipeline.project).to receive(:ci_config_path) { nil }
+
+ is_expected.to eq('.gitlab-ci.yml')
+ end
- it 'returns default when custom path is nil' do
- allow(pipeline.project).to receive(:ci_config_path) { nil }
+ it 'returns default when custom path is empty' do
+ allow(pipeline.project).to receive(:ci_config_path) { '' }
- is_expected.to eq('.gitlab-ci.yml')
+ is_expected.to eq('.gitlab-ci.yml')
+ end
+ end
end
- it 'returns default when custom path is empty' do
- allow(pipeline.project).to receive(:ci_config_path) { '' }
+ context 'when pipeline is for auto-devops' do
+ before do
+ pipeline.config_source = 'auto_devops_source'
+ end
- is_expected.to eq('.gitlab-ci.yml')
+ it 'does not return config file' do
+ is_expected.to be_nil
+ end
end
end
@@ -1835,6 +2081,55 @@ describe Ci::Pipeline, :mailer do
expect(pipeline.all_merge_requests).to be_empty
end
+
+ context 'when there is a merge request pipeline' do
+ let(:source_branch) { 'feature' }
+ let(:target_branch) { 'master' }
+
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: project,
+ ref: source_branch,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch)
+ end
+
+ it 'returns an associated merge request' do
+ expect(pipeline.all_merge_requests).to eq([merge_request])
+ end
+
+ context 'when there is another merge request pipeline that targets a different branch' do
+ let(:target_branch_2) { 'merge-test' }
+
+ let!(:pipeline_2) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: project,
+ ref: source_branch,
+ merge_request: merge_request_2)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch_2)
+ end
+
+ it 'does not return an associated merge request' do
+ expect(pipeline.all_merge_requests).not_to include(merge_request_2)
+ end
+ end
+ end
end
describe '#stuck?' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index b545e036aa1..ad79f8d4ce0 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::Runner do
+ it_behaves_like 'having unique enum values'
+
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) }
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 5076f7faeac..3228c400155 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe Ci::Stage, :models do
let(:stage) { create(:ci_stage_entity) }
+ it_behaves_like 'having unique enum values'
+
describe 'associations' do
before do
create(:ci_build, stage_id: stage.id)
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index cfe0e216c78..cd28f1fe9c6 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -3,6 +3,8 @@ require 'rails_helper'
describe Clusters::Applications::Ingress do
let(:ingress) { create(:clusters_applications_ingress) }
+ it_behaves_like 'having unique enum values'
+
include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status specs', :clusters_applications_ingress
include_examples 'cluster application helm specs', :clusters_applications_ingress
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/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index eb68ebccdcb..840f74c9890 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Clusters::Cluster do
+ it_behaves_like 'having unique enum values'
+
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
@@ -90,6 +92,26 @@ describe Clusters::Cluster do
it { is_expected.to contain_exactly(cluster) }
end
+ describe '.missing_kubernetes_namespace' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :project) }
+ let(:project) { cluster.project }
+ let(:kubernetes_namespaces) { project.kubernetes_namespaces }
+
+ subject do
+ described_class.joins(:projects).where(projects: { id: project.id }).missing_kubernetes_namespace(kubernetes_namespaces)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+
+ context 'kubernetes namespace exists' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe 'validation' do
subject { cluster.valid? }
@@ -231,6 +253,81 @@ describe Clusters::Cluster do
end
end
+ describe '.ancestor_clusters_for_clusterable' do
+ let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:group) { group_cluster.group }
+ let(:hierarchy_order) { :desc }
+ let(:clusterable) { project }
+
+ subject do
+ described_class.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: hierarchy_order)
+ end
+
+ context 'when project does not belong to this group' do
+ let(:project) { create(:project, group: create(:group)) }
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when group has a configured kubernetes cluster' do
+ let(:project) { create(:project, group: group) }
+
+ it 'returns the group cluster' do
+ is_expected.to eq([group_cluster])
+ end
+ end
+
+ context 'when sub-group has configured kubernetes cluster', :nested_groups do
+ let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:sub_group) { sub_group_cluster.group }
+ let(:project) { create(:project, group: sub_group) }
+
+ before do
+ sub_group.update!(parent: group)
+ end
+
+ it 'returns clusters in order, descending the hierachy' do
+ is_expected.to eq([group_cluster, sub_group_cluster])
+ end
+
+ it 'avoids N+1 queries' do
+ another_project = create(:project)
+ control_count = ActiveRecord::QueryRecorder.new do
+ described_class.ancestor_clusters_for_clusterable(another_project, hierarchy_order: hierarchy_order)
+ end.count
+
+ cluster2 = create(:cluster, :provided_by_gcp, :group)
+ child2 = cluster2.group
+ child2.update!(parent: sub_group)
+ project = create(:project, group: child2)
+
+ expect do
+ described_class.ancestor_clusters_for_clusterable(project, hierarchy_order: hierarchy_order)
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ context 'for a group' do
+ let(:clusterable) { sub_group }
+
+ it 'returns clusters in order for a group' do
+ is_expected.to eq([group_cluster])
+ end
+ end
+ end
+
+ context 'scope chaining' do
+ let(:project) { create(:project, group: group) }
+
+ subject { described_class.none.ancestor_clusters_for_clusterable(project) }
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '#provider' do
subject { cluster.provider }
@@ -263,6 +360,31 @@ describe Clusters::Cluster do
end
end
+ describe '#all_projects' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, projects: [project]) }
+
+ subject { cluster.all_projects }
+
+ context 'project cluster' do
+ it 'returns project' do
+ is_expected.to eq([project])
+ end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, group: group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subproject) { create(:project, group: subgroup) }
+
+ it 'returns all projects for group' do
+ is_expected.to contain_exactly(project, subproject)
+ end
+ end
+ end
+
describe '#first_project' do
subject { cluster.first_project }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 99fd6ccc4d8..062d2fd0768 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -18,6 +18,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to delegate_method(:managed?).to(:cluster) }
it { is_expected.to delegate_method(:kubernetes_namespace).to(:cluster) }
+ it_behaves_like 'having unique enum values'
+
describe 'before_validation' do
context 'when namespace includes upper case' do
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
@@ -273,6 +275,36 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
)
end
end
+
+ context 'group level cluster' do
+ let!(:cluster) { create(:cluster, :group, platform_kubernetes: kubernetes) }
+
+ let(:project) { create(:project, group: cluster.group) }
+
+ subject { kubernetes.predefined_variables(project: project) }
+
+ context 'no kubernetes namespace for the project' do
+ it_behaves_like 'setting variables'
+
+ it 'does not return KUBE_TOKEN' do
+ expect(subject).not_to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ )
+ end
+ end
+
+ context 'kubernetes namespace exists for the project' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, :with_token, cluster: cluster, project: project) }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ )
+ end
+ end
+ end
end
describe '#terminals' do
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/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 917685399d4..8b7c88805c1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -13,6 +13,8 @@ describe CommitStatus do
create(:commit_status, pipeline: pipeline, **opts)
end
+ it_behaves_like 'having unique enum values'
+
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
index 8847623f705..b14b773b653 100644
--- a/spec/models/concerns/chronic_duration_attribute_spec.rb
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -54,7 +54,8 @@ shared_examples 'ChronicDurationAttribute writer' do
subject.send("#{virtual_field}=", '-10m')
expect(subject.valid?).to be_falsey
- expect(subject.errors&.messages).to include(virtual_field => ['is not a correct duration'])
+ expect(subject.errors&.messages)
+ .to include(base: ['Maximum job timeout has a value which could not be accepted'])
end
end
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 7bb89fe41dc..19ab4382b53 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -43,13 +43,86 @@ describe DeploymentPlatform do
it { is_expected.to be_nil }
end
- context 'when user configured kubernetes from CI/CD > Clusters' do
+ context 'when project has configured kubernetes from CI/CD > Clusters' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:platform_kubernetes) { cluster.platform_kubernetes }
it 'returns the Kubernetes platform' do
expect(subject).to eq(platform_kubernetes)
end
+
+ context 'with a group level kubernetes cluster' do
+ let(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ before do
+ project.update!(group: group_cluster.group)
+ end
+
+ it 'returns the Kubernetes platform from the project cluster' do
+ expect(subject).to eq(platform_kubernetes)
+ end
+ end
+ end
+
+ context 'when group has configured kubernetes cluster' do
+ let!(:group_cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:group) { group_cluster.group }
+
+ before do
+ project.update!(group: group)
+ end
+
+ it 'returns the Kubernetes platform' do
+ is_expected.to eq(group_cluster.platform_kubernetes)
+ end
+
+ context 'when child group has configured kubernetes cluster', :nested_groups do
+ let!(:child_group1_cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:child_group1) { child_group1_cluster.group }
+
+ before do
+ project.update!(group: child_group1)
+ child_group1.update!(parent: group)
+ end
+
+ it 'returns the Kubernetes platform for the child group' do
+ is_expected.to eq(child_group1_cluster.platform_kubernetes)
+ end
+
+ context 'deeply nested group' do
+ let!(:child_group2_cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:child_group2) { child_group2_cluster.group }
+
+ before do
+ child_group2.update!(parent: child_group1)
+ project.update!(group: child_group2)
+ end
+
+ it 'returns most nested group cluster Kubernetes platform' do
+ is_expected.to eq(child_group2_cluster.platform_kubernetes)
+ end
+
+ context 'cluster in the middle of hierarchy is disabled' do
+ before do
+ child_group2_cluster.update!(enabled: false)
+ end
+
+ it 'returns closest enabled Kubenetes platform' do
+ is_expected.to eq(child_group1_cluster.platform_kubernetes)
+ end
+ end
+ end
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(group_clusters: false)
+ end
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
end
context 'when user configured kubernetes integration from project services' 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 782687516ae..55d83bc3a6b 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -21,44 +21,59 @@ end
describe ApplicationSetting, 'TokenAuthenticatable' do
let(:token_field) { :runners_registration_token }
+ let(:settings) { described_class.new }
+
it_behaves_like 'TokenAuthenticatable'
describe 'generating new token' do
context 'token is not generated yet' do
describe 'token field accessor' do
- subject { described_class.new.send(token_field) }
+ subject { settings.send(token_field) }
+
it { is_expected.not_to be_blank }
end
- describe 'ensured token' do
- subject { described_class.new.send("ensure_#{token_field}") }
+ describe "ensure_runners_registration_token" do
+ subject { settings.send("ensure_#{token_field}") }
it { is_expected.to be_a String }
it { is_expected.not_to be_blank }
+
+ it 'does not persist token' do
+ expect(settings).not_to be_persisted
+ end
end
- describe 'ensured! token' do
- subject { described_class.new.send("ensure_#{token_field}!") }
+ describe 'ensure_runners_registration_token!' do
+ subject { settings.send("ensure_#{token_field}!") }
- it 'persists new token' do
- expect(subject).to eq described_class.current[token_field]
+ it 'persists new token as an encrypted string' do
+ expect(subject).to eq settings.reload.runners_registration_token
+ expect(settings.read_attribute('runners_registration_token_encrypted'))
+ .to eq Gitlab::CryptoHelper.aes256_gcm_encrypt(subject)
+ expect(settings).to be_persisted
+ end
+
+ it 'does not persist token in a clear text' do
+ expect(subject).not_to eq settings.reload
+ .read_attribute('runners_registration_token_encrypted')
end
end
end
context 'token is generated' do
before do
- subject.send("reset_#{token_field}!")
+ settings.send("reset_#{token_field}!")
end
it 'persists a new token' do
- expect(subject.send(:read_attribute, token_field)).to be_a String
+ expect(settings.runners_registration_token).to be_a String
end
end
end
describe 'setting new token' do
- subject { described_class.new.send("set_#{token_field}", '0123456789') }
+ subject { settings.send("set_#{token_field}", '0123456789') }
it { is_expected.to eq '0123456789' }
end
@@ -336,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/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
new file mode 100644
index 00000000000..6605f1f5a5f
--- /dev/null
+++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe TokenAuthenticatableStrategies::Base do
+ let(:instance) { double(:instance) }
+ let(:field) { double(:field) }
+
+ describe '.fabricate' do
+ context 'when digest stragegy is specified' do
+ it 'fabricates digest strategy object' do
+ strategy = described_class.fabricate(instance, field, digest: true)
+
+ expect(strategy).to be_a TokenAuthenticatableStrategies::Digest
+ end
+ end
+
+ context 'when encrypted strategy is specified' do
+ it 'fabricates encrypted strategy object' do
+ strategy = described_class.fabricate(instance, field, encrypted: true)
+
+ expect(strategy).to be_a TokenAuthenticatableStrategies::Encrypted
+ end
+ end
+
+ context 'when no strategy is specified' do
+ it 'fabricates insecure strategy object' do
+ strategy = described_class.fabricate(instance, field, something: true)
+
+ expect(strategy).to be_a TokenAuthenticatableStrategies::Insecure
+ end
+ end
+
+ context 'when incompatible options are provided' do
+ it 'raises an error' do
+ expect { described_class.fabricate(instance, field, digest: true, encrypted: true) }
+ .to raise_error ArgumentError
+ end
+ end
+ end
+
+ describe '#fallback?' do
+ context 'when fallback is set' do
+ it 'recognizes fallback setting' do
+ strategy = described_class.new(instance, field, fallback: true)
+
+ expect(strategy.fallback?).to be true
+ end
+ end
+
+ context 'when fallback is not a valid value' do
+ it 'raises an error' do
+ strategy = described_class.new(instance, field, fallback: 'something')
+
+ expect { strategy.fallback? }.to raise_error ArgumentError
+ end
+ end
+
+ context 'when fallback is not set' do
+ it 'raises an error' do
+ strategy = described_class.new(instance, field, {})
+
+ expect(strategy.fallback?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
new file mode 100644
index 00000000000..93cab80cb1f
--- /dev/null
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -0,0 +1,156 @@
+require 'spec_helper'
+
+describe TokenAuthenticatableStrategies::Encrypted do
+ let(:model) { double(:model) }
+ let(:instance) { double(:instance) }
+
+ let(:encrypted) do
+ Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value')
+ end
+
+ subject do
+ described_class.new(model, 'some_field', options)
+ end
+
+ describe '.new' do
+ context 'when fallback and migration strategies are set' do
+ let(:options) { { fallback: true, migrating: true } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error ArgumentError, /not compatible/
+ end
+ end
+ end
+
+ describe '#find_token_authenticatable' do
+ context 'when using fallback strategy' do
+ let(:options) { { fallback: true } }
+
+ it 'finds the encrypted resource by cleartext' do
+ allow(model).to receive(:find_by)
+ .with('some_field_encrypted' => encrypted)
+ .and_return('encrypted resource')
+
+ expect(subject.find_token_authenticatable('my-value'))
+ .to eq 'encrypted resource'
+ end
+
+ it 'uses insecure strategy when encrypted token cannot be found' do
+ allow(subject.send(:insecure_strategy))
+ .to receive(:find_token_authenticatable)
+ .and_return('plaintext resource')
+
+ allow(model).to receive(:find_by)
+ .with('some_field_encrypted' => encrypted)
+ .and_return(nil)
+
+ expect(subject.find_token_authenticatable('my-value'))
+ .to eq 'plaintext resource'
+ end
+ end
+
+ context 'when using migration strategy' do
+ let(:options) { { migrating: true } }
+
+ it 'finds the cleartext resource by cleartext' do
+ allow(model).to receive(:find_by)
+ .with('some_field' => 'my-value')
+ .and_return('cleartext resource')
+
+ expect(subject.find_token_authenticatable('my-value'))
+ .to eq 'cleartext resource'
+ end
+
+ it 'returns nil if resource cannot be found' do
+ allow(model).to receive(:find_by)
+ .with('some_field' => 'my-value')
+ .and_return(nil)
+
+ expect(subject.find_token_authenticatable('my-value'))
+ .to be_nil
+ end
+ end
+ end
+
+ describe '#get_token' do
+ context 'when using fallback strategy' do
+ let(:options) { { fallback: true } }
+
+ it 'returns decrypted token when an encrypted token is present' do
+ allow(instance).to receive(:read_attribute)
+ .with('some_field_encrypted')
+ .and_return(encrypted)
+
+ expect(subject.get_token(instance)).to eq 'my-value'
+ end
+
+ it 'returns the plaintext token when encrypted token is not present' do
+ allow(instance).to receive(:read_attribute)
+ .with('some_field_encrypted')
+ .and_return(nil)
+
+ allow(instance).to receive(:read_attribute)
+ .with('some_field')
+ .and_return('cleartext value')
+
+ expect(subject.get_token(instance)).to eq 'cleartext value'
+ end
+ end
+
+ context 'when using migration strategy' do
+ let(:options) { { migrating: true } }
+
+ it 'returns cleartext token when an encrypted token is present' do
+ allow(instance).to receive(:read_attribute)
+ .with('some_field_encrypted')
+ .and_return(encrypted)
+
+ allow(instance).to receive(:read_attribute)
+ .with('some_field')
+ .and_return('my-cleartext-value')
+
+ expect(subject.get_token(instance)).to eq 'my-cleartext-value'
+ end
+
+ it 'returns the cleartext token when encrypted token is not present' do
+ allow(instance).to receive(:read_attribute)
+ .with('some_field_encrypted')
+ .and_return(nil)
+
+ allow(instance).to receive(:read_attribute)
+ .with('some_field')
+ .and_return('cleartext value')
+
+ expect(subject.get_token(instance)).to eq 'cleartext value'
+ end
+ end
+ end
+
+ describe '#set_token' do
+ context 'when using fallback strategy' do
+ let(:options) { { fallback: true } }
+
+ it 'writes encrypted token and removes plaintext token and returns it' do
+ expect(instance).to receive(:[]=)
+ .with('some_field_encrypted', encrypted)
+ expect(instance).to receive(:[]=)
+ .with('some_field', nil)
+
+ expect(subject.set_token(instance, 'my-value')).to eq 'my-value'
+ end
+ end
+
+ context 'when using migration strategy' do
+ let(:options) { { migrating: true } }
+
+ it 'writes encrypted token and writes plaintext token' do
+ expect(instance).to receive(:[]=)
+ .with('some_field_encrypted', encrypted)
+ expect(instance).to receive(:[]=)
+ .with('some_field', 'my-value')
+
+ expect(subject.set_token(instance, 'my-value')).to eq 'my-value'
+ end
+ end
+ end
+end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 270b2767c68..a8d53cfcd7d 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,8 @@ describe Deployment do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ it_behaves_like 'having unique enum values'
+
describe '#scheduled_actions' do
subject { deployment.scheduled_actions }
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 90f7e4a4590..9da16dea929 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -92,16 +92,12 @@ describe EnvironmentStatus do
end
describe '.build_environments_status' do
- subject { described_class.send(:build_environments_status, merge_request, user, sha) }
+ subject { described_class.send(:build_environments_status, merge_request, user, pipeline) }
let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
let(:environment) { build.deployment.environment }
let(:user) { project.owner }
- before do
- build.deployment&.update!(sha: sha)
- end
-
context 'when environment is created on a forked project' do
let(:project) { create(:project, :repository) }
let(:forked) { fork_project(project, user, repository: true) }
@@ -160,6 +156,39 @@ describe EnvironmentStatus do
expect(subject.count).to eq(0)
end
end
+
+ context 'when multiple deployments with the same SHA in different environments' do
+ let(:pipeline2) { create(:ci_pipeline, sha: sha, project: project) }
+ let!(:build2) { create(:ci_build, :start_review_app, pipeline: pipeline2) }
+
+ it 'returns deployments related to the head pipeline' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+ end
+
+ context 'when multiple deployments in the same pipeline for the same environments' do
+ let!(:build2) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+
+ it 'returns unique entries' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+ end
+
+ context 'when environment is stopped' do
+ before do
+ environment.stop!
+ end
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
end
end
end
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index 0136bb61c07..cdd7dea2064 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe GpgSignature do
let(:gpg_key) { create(:gpg_key) }
let(:gpg_key_subkey) { create(:gpg_key_subkey) }
+ it_behaves_like 'having unique enum values'
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:gpg_key) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ada00f03928..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,10 +739,39 @@ 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 }
end
end
+
+ describe '#group_clusters_enabled?' do
+ before do
+ # Override global stub in spec/spec_helper.rb
+ expect(Feature).to receive(:enabled?).and_call_original
+ end
+
+ subject { group.group_clusters_enabled? }
+
+ it { is_expected.to be_truthy }
+
+ context 'explicitly disabled for root ancestor' do
+ before do
+ feature = Feature.get(:group_clusters)
+ feature.disable(group.root_ancestor)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'explicitly disabled for root ancestor' do
+ before do
+ feature = Feature.get(:group_clusters)
+ feature.enable(group.root_ancestor)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 52c00a74b4b..4696341c05f 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -7,6 +7,8 @@ describe InternalId do
let(:scope) { { project: project } }
let(:init) { ->(s) { s.project.issues.size } }
+ it_behaves_like 'having unique enum values'
+
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index 17dc27bd132..a51580f8292 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe List do
+ it_behaves_like 'having unique enum values'
+
describe 'relationships' do
it { is_expected.to belong_to(:board) }
it { is_expected.to belong_to(:label) }
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..097b1bb30dc 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -124,4 +124,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 ad55c280399..9b60054e14a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1206,6 +1206,119 @@ describe MergeRequest do
expect(subject.all_pipelines).to contain_exactly(pipeline)
end
end
+
+ context 'when pipelines exist for the branch and merge request' do
+ let(:source_ref) { 'feature' }
+ let(:target_ref) { 'master' }
+
+ let!(:branch_pipeline) do
+ create(:ci_pipeline,
+ source: :push,
+ project: project,
+ ref: source_ref,
+ sha: shas.second)
+ end
+
+ let!(:merge_request_pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: project,
+ ref: source_ref,
+ sha: shas.second,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: source_ref,
+ target_project: project,
+ target_branch: target_ref)
+ end
+
+ let(:project) { create(:project, :repository) }
+ let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) }
+
+ before do
+ allow(merge_request).to receive(:all_commit_shas) { shas }
+ end
+
+ it 'returns merge request pipeline first' do
+ expect(merge_request.all_pipelines)
+ .to eq([merge_request_pipeline,
+ branch_pipeline])
+ end
+
+ context 'when there are a branch pipeline and a merge request pipeline' do
+ let!(:branch_pipeline_2) do
+ create(:ci_pipeline,
+ source: :push,
+ project: project,
+ ref: source_ref,
+ sha: shas.first)
+ end
+
+ let!(:merge_request_pipeline_2) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: project,
+ ref: source_ref,
+ sha: shas.first,
+ merge_request: merge_request)
+ end
+
+ it 'returns merge request pipelines first' do
+ expect(merge_request.all_pipelines)
+ .to eq([merge_request_pipeline_2,
+ merge_request_pipeline,
+ branch_pipeline_2,
+ branch_pipeline])
+ end
+ end
+
+ context 'when there are multiple merge request pipelines from the same branch' do
+ let!(:branch_pipeline_2) do
+ create(:ci_pipeline,
+ source: :push,
+ project: project,
+ ref: source_ref,
+ sha: shas.first)
+ end
+
+ let!(:merge_request_pipeline_2) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: project,
+ ref: source_ref,
+ sha: shas.first,
+ merge_request: merge_request_2)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: source_ref,
+ target_project: project,
+ target_branch: 'stable')
+ end
+
+ before do
+ allow(merge_request_2).to receive(:all_commit_shas) { shas }
+ end
+
+ it 'returns only related merge request pipelines' do
+ expect(merge_request.all_pipelines)
+ .to eq([merge_request_pipeline,
+ branch_pipeline_2,
+ branch_pipeline])
+
+ expect(merge_request_2.all_pipelines)
+ .to eq([merge_request_pipeline_2,
+ branch_pipeline_2,
+ branch_pipeline])
+ end
+ end
+ end
end
describe '#has_test_reports?' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 2db42fe802a..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)
@@ -560,6 +560,7 @@ describe Namespace do
let!(:project2) { create(:project_empty_repo, namespace: child) }
it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
+ it { expect(child.all_projects.to_a).to match_array([project2]) }
end
describe '#all_pipelines' do
@@ -720,6 +721,7 @@ describe Namespace do
deep_nested_group = create(:group, parent: nested_group)
very_deep_nested_group = create(:group, parent: deep_nested_group)
+ expect(root_group.root_ancestor).to eq(root_group)
expect(nested_group.root_ancestor).to eq(root_group)
expect(deep_nested_group.root_ancestor).to eq(root_group)
expect(very_deep_nested_group.root_ancestor).to eq(root_group)
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index f9be61e4768..bcdfe3cf1eb 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -517,7 +517,7 @@ describe Note do
describe '#to_ability_name' do
it 'returns snippet for a project snippet note' do
- expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
+ expect(build(:note_on_project_snippet).to_ability_name).to eq('project_snippet')
end
it 'returns personal_snippet for a personal snippet note' do
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index e545b674b4f..771d834c4bc 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe NotificationSetting do
+ it_behaves_like 'having unique enum values'
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:source) }
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
new file mode 100644
index 00000000000..541e78507e5
--- /dev/null
+++ b/spec/models/pool_repository_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PoolRepository do
+ describe 'associations' do
+ it { is_expected.to belong_to(:shard) }
+ it { is_expected.to have_many(:member_projects) }
+ end
+
+ describe 'validations' do
+ let!(:pool_repository) { create(:pool_repository) }
+
+ it { is_expected.to validate_presence_of(:shard) }
+ 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,}/) )
+ end
+ end
+end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 342798f730b..7ff64c76e37 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe ProjectAutoDevops do
set(:project) { build(:project) }
+ it_behaves_like 'having unique enum values'
+
it { is_expected.to belong_to(:project) }
it { is_expected.to define_enum_for(:deploy_strategy) }
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index f7033b28c76..e3b2d971419 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -10,4 +10,116 @@ describe ProjectImportState, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
end
+
+ describe 'Project import job' do
+ let(:import_state) { create(:import_state, import_url: generate(:url)) }
+ let(:project) { import_state.project }
+
+ before do
+ allow_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository)
+ .with(project.import_url).and_return(true)
+
+ # Works around https://github.com/rspec/rspec-mocks/issues/910
+ allow(Project).to receive(:find).with(project.id).and_return(project)
+ expect(project.repository).to receive(:after_import).and_call_original
+ expect(project.wiki.repository).to receive(:after_import).and_call_original
+ end
+
+ it 'imports a project' do
+ expect(RepositoryImportWorker).to receive(:perform_async).and_call_original
+
+ expect { import_state.schedule }.to change { import_state.jid }
+ expect(import_state.status).to eq('finished')
+ end
+ end
+
+ describe '#human_status_name' do
+ context 'when import_state exists' do
+ it 'returns the humanized status name' do
+ import_state = build(:import_state, :started)
+
+ expect(import_state.human_status_name).to eq("started")
+ end
+ end
+ end
+
+ describe 'import state transitions' do
+ context 'state transition: [:started] => [:finished]' do
+ let(:after_import_service) { spy(:after_import_service) }
+ let(:housekeeping_service) { spy(:housekeeping_service) }
+
+ before do
+ allow(Projects::AfterImportService)
+ .to receive(:new) { after_import_service }
+
+ allow(after_import_service)
+ .to receive(:execute) { housekeeping_service.execute }
+
+ allow(Projects::HousekeepingService)
+ .to receive(:new) { housekeeping_service }
+ end
+
+ it 'resets last_error' do
+ error_message = 'Some error'
+ import_state = create(:import_state, :started, last_error: error_message)
+
+ expect { import_state.finish }.to change { import_state.last_error }.from(error_message).to(nil)
+ end
+
+ it 'performs housekeeping when an import of a fresh project is completed' do
+ project = create(:project_empty_repo, :import_started, import_type: :github)
+
+ project.import_state.finish
+
+ expect(after_import_service).to have_received(:execute)
+ expect(housekeeping_service).to have_received(:execute)
+ end
+
+ it 'does not perform housekeeping when project repository does not exist' do
+ project = create(:project, :import_started, import_type: :github)
+
+ project.import_state.finish
+
+ expect(housekeeping_service).not_to have_received(:execute)
+ end
+
+ it 'does not perform housekeeping when project does not have a valid import type' do
+ project = create(:project, :import_started, import_type: nil)
+
+ project.import_state.finish
+
+ expect(housekeeping_service).not_to have_received(:execute)
+ end
+ end
+ end
+
+ describe '#remove_jid', :clean_gitlab_redis_cache do
+ let(:project) { }
+
+ context 'without an JID' do
+ it 'does nothing' do
+ import_state = create(:import_state)
+
+ expect(Gitlab::SidekiqStatus)
+ .not_to receive(:unset)
+
+ import_state.remove_jid
+ end
+ end
+
+ context 'with an JID' do
+ it 'unsets the JID' do
+ import_state = create(:import_state, jid: '123')
+
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:unset)
+ .with('123')
+ .and_call_original
+
+ import_state.remove_jid
+
+ expect(import_state.jid).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/project_repository_spec.rb b/spec/models/project_repository_spec.rb
new file mode 100644
index 00000000000..c966447fedc
--- /dev/null
+++ b/spec/models/project_repository_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectRepository do
+ describe 'associations' do
+ it { is_expected.to belong_to(:shard) }
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe '.find_project' do
+ it 'finds project by disk path' do
+ project = create(:project)
+ project.track_project_repository
+
+ expect(described_class.find_project(project.disk_path)).to eq(project)
+ end
+
+ it 'returns nil when it does not find the project' do
+ expect(described_class.find_project('@@unexisting/path/to/project')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index f2cb927df37..b6cf4c72450 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -13,6 +13,23 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
it { is_expected.to belong_to :project }
end
+ context 'redirects' do
+ it 'does not follow redirects' do
+ redirect_to = 'https://redirected.example.com'
+ redirect_req_stub = stub_prometheus_request(prometheus_query_url('1'), status: 302, headers: { location: redirect_to })
+ redirected_req_stub = stub_prometheus_request(redirect_to, body: { 'status': 'success' })
+
+ result = service.test
+
+ # result = { success: false, result: error }
+ expect(result[:success]).to be_falsy
+ expect(result[:result]).to be_instance_of(Gitlab::PrometheusClient::Error)
+
+ expect(redirect_req_stub).to have_been_requested
+ expect(redirected_req_stub).not_to have_been_requested
+ end
+ end
+
describe 'Validations' do
context 'when manual_configuration is enabled' do
before do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 47c331fbc7a..93c83fd21fd 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -4,6 +4,8 @@ describe Project do
include ProjectForksHelper
include GitHelpers
+ it_behaves_like 'having unique enum values'
+
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
@@ -54,13 +56,14 @@ describe Project do
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:project_feature) }
+ it { is_expected.to have_one(:project_repository) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:fork_network_member) }
it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
it { is_expected.to have_many(:commit_statuses) }
- it { is_expected.to have_many(:pipelines) }
+ it { is_expected.to have_many(:ci_pipelines) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
@@ -84,6 +87,7 @@ describe Project do
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
+ it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) }
@@ -139,6 +143,29 @@ describe Project do
expect(subject.boards.size).to eq 1
end
end
+
+ describe 'ci_pipelines association' do
+ context 'when feature flag pipeline_ci_sources_only is enabled' do
+ it 'returns only pipelines from ci_sources' do
+ stub_feature_flags(pipeline_ci_sources_only: true)
+
+ expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
+
+ subject.ci_pipelines
+ end
+ end
+
+ context 'when feature flag pipeline_ci_sources_only is disabled' do
+ it 'returns all pipelines' do
+ stub_feature_flags(pipeline_ci_sources_only: false)
+
+ expect(Ci::Pipeline).not_to receive(:ci_sources).and_call_original
+ expect(Ci::Pipeline).to receive(:all).and_call_original.at_least(:once)
+
+ subject.ci_pipelines
+ end
+ end
+ end
end
describe 'modules' do
@@ -151,6 +178,24 @@ describe Project do
it { is_expected.to include_module(Sortable) }
end
+ describe '.missing_kubernetes_namespace' do
+ let!(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :provided_by_user, :group) }
+ let(:kubernetes_namespaces) { project.kubernetes_namespaces }
+
+ subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) }
+
+ it { is_expected.to contain_exactly(project) }
+
+ context 'kubernetes namespace exists' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe 'validation' do
let!(:project) { create(:project) }
@@ -218,76 +263,93 @@ describe Project do
end
end
- it 'does not allow an invalid URI as import_url' do
- project = build(:project, import_url: 'invalid://')
+ describe 'import_url' do
+ it 'does not allow an invalid URI as import_url' do
+ project = build(:project, import_url: 'invalid://')
- expect(project).not_to be_valid
- end
+ expect(project).not_to be_valid
+ end
- it 'does allow a SSH URI as import_url for persisted projects' do
- project = create(:project)
- project.import_url = 'ssh://test@gitlab.com/project.git'
+ it 'does allow a SSH URI as import_url for persisted projects' do
+ project = create(:project)
+ project.import_url = 'ssh://test@gitlab.com/project.git'
- expect(project).to be_valid
- end
+ expect(project).to be_valid
+ end
- it 'does not allow a SSH URI as import_url for new projects' do
- project = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
+ it 'does not allow a SSH URI as import_url for new projects' do
+ project = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
- expect(project).not_to be_valid
- end
+ expect(project).not_to be_valid
+ end
- it 'does allow a valid URI as import_url' do
- project = build(:project, import_url: 'http://gitlab.com/project.git')
+ it 'does allow a valid URI as import_url' do
+ project = build(:project, import_url: 'http://gitlab.com/project.git')
- expect(project).to be_valid
- end
+ expect(project).to be_valid
+ end
- it 'allows an empty URI' do
- project = build(:project, import_url: '')
+ it 'allows an empty URI' do
+ project = build(:project, import_url: '')
- expect(project).to be_valid
- end
+ expect(project).to be_valid
+ end
- it 'does not produce import data on an empty URI' do
- project = build(:project, import_url: '')
+ it 'does not produce import data on an empty URI' do
+ project = build(:project, import_url: '')
- expect(project.import_data).to be_nil
- end
+ expect(project.import_data).to be_nil
+ end
- it 'does not produce import data on an invalid URI' do
- project = build(:project, import_url: 'test://')
+ it 'does not produce import data on an invalid URI' do
+ project = build(:project, import_url: 'test://')
- expect(project.import_data).to be_nil
- end
+ expect(project.import_data).to be_nil
+ end
- it "does not allow import_url pointing to localhost" do
- project = build(:project, import_url: 'http://localhost:9000/t.git')
+ it "does not allow import_url pointing to localhost" do
+ project = build(:project, import_url: 'http://localhost:9000/t.git')
- expect(project).to be_invalid
- expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
- end
+ expect(project).to be_invalid
+ expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
+ end
- it "does not allow import_url with invalid ports for new projects" do
- project = build(:project, import_url: 'http://github.com:25/t.git')
+ it "does not allow import_url with invalid ports for new projects" do
+ project = build(:project, import_url: 'http://github.com:25/t.git')
- expect(project).to be_invalid
- expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443')
- end
+ expect(project).to be_invalid
+ expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443')
+ end
- it "does not allow import_url with invalid ports for persisted projects" do
- project = create(:project)
- project.import_url = 'http://github.com:25/t.git'
+ it "does not allow import_url with invalid ports for persisted projects" do
+ project = create(:project)
+ project.import_url = 'http://github.com:25/t.git'
- expect(project).to be_invalid
- expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
- end
+ expect(project).to be_invalid
+ expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
+ end
- it "does not allow import_url with invalid user" do
- project = build(:project, import_url: 'http://$user:password@github.com/t.git')
+ it "does not allow import_url with invalid user" do
+ project = build(:project, import_url: 'http://$user:password@github.com/t.git')
- expect(project).to be_invalid
- expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
+ expect(project).to be_invalid
+ expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
+ end
+
+ include_context 'invalid urls'
+
+ it 'does not allow urls with CR or LF characters' do
+ project = build(:project)
+
+ aggregate_failures do
+ urls_with_CRLF.each do |url|
+ project.import_url = url
+
+ expect(project).not_to be_valid
+ expect(project.errors.full_messages.first).to match(/is blocked: URI is invalid/)
+ end
+ end
+ end
end
describe 'project pending deletion' do
@@ -373,6 +435,8 @@ describe Project do
it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
end
describe '#to_reference_with_postfix' do
@@ -1601,6 +1665,30 @@ describe Project do
end
end
+ describe '#track_project_repository' do
+ let(:project) { create(:project, :repository) }
+
+ it 'creates a project_repository' do
+ project.track_project_repository
+
+ expect(project.reload.project_repository).to be_present
+ expect(project.project_repository.disk_path).to eq(project.disk_path)
+ expect(project.project_repository.shard_name).to eq(project.repository_storage)
+ end
+
+ it 'updates the project_repository' do
+ project.track_project_repository
+
+ allow(project).to receive(:disk_path).and_return('@fancy/new/path')
+
+ expect do
+ project.track_project_repository
+ end.not_to change(ProjectRepository, :count)
+
+ expect(project.reload.project_repository.disk_path).to eq(project.disk_path)
+ end
+ end
+
describe '#create_repository' do
let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
@@ -1686,6 +1774,16 @@ describe Project do
end
end
+ describe 'handling import URL' do
+ it 'returns the sanitized URL' do
+ project = create(:project, :import_started, import_url: 'http://user:pass@test.com')
+
+ project.import_state.finish
+
+ expect(project.reload.import_url).to eq('http://test.com')
+ end
+ end
+
describe '#container_registry_url' do
let(:project) { create(:project) }
@@ -1799,106 +1897,6 @@ describe Project do
end
end
- describe '#human_import_status_name' do
- context 'when import_state exists' do
- it 'returns the humanized status name' do
- project = create(:project)
- create(:import_state, :started, project: project)
-
- expect(project.human_import_status_name).to eq("started")
- end
- end
-
- context 'when import_state was not created yet' do
- let(:project) { create(:project, :import_started) }
-
- it 'ensures import_state is created and returns humanized status name' do
- expect do
- project.human_import_status_name
- end.to change { ProjectImportState.count }.from(0).to(1)
- end
-
- it 'returns humanized status name' do
- expect(project.human_import_status_name).to eq("started")
- end
- end
- end
-
- describe 'Project import job' do
- let(:project) { create(:project, import_url: generate(:url)) }
-
- before do
- allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
- .with(project.repository_storage, project.disk_path, project.import_url)
- .and_return(true)
-
- # Works around https://github.com/rspec/rspec-mocks/issues/910
- allow(described_class).to receive(:find).with(project.id).and_return(project)
- expect(project.repository).to receive(:after_import)
- .and_call_original
- expect(project.wiki.repository).to receive(:after_import)
- .and_call_original
- end
-
- it 'imports a project' do
- expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
-
- expect { project.import_schedule }.to change { project.import_jid }
- expect(project.reload.import_status).to eq('finished')
- end
- end
-
- describe 'project import state transitions' do
- context 'state transition: [:started] => [:finished]' do
- let(:after_import_service) { spy(:after_import_service) }
- let(:housekeeping_service) { spy(:housekeeping_service) }
-
- before do
- allow(Projects::AfterImportService)
- .to receive(:new) { after_import_service }
-
- allow(after_import_service)
- .to receive(:execute) { housekeeping_service.execute }
-
- allow(Projects::HousekeepingService)
- .to receive(:new) { housekeeping_service }
- end
-
- it 'resets project import_error' do
- error_message = 'Some error'
- mirror = create(:project_empty_repo, :import_started)
- mirror.import_state.update(last_error: error_message)
-
- expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
- end
-
- it 'performs housekeeping when an import of a fresh project is completed' do
- project = create(:project_empty_repo, :import_started, import_type: :github)
-
- project.import_finish
-
- expect(after_import_service).to have_received(:execute)
- expect(housekeeping_service).to have_received(:execute)
- end
-
- it 'does not perform housekeeping when project repository does not exist' do
- project = create(:project, :import_started, import_type: :github)
-
- project.import_finish
-
- expect(housekeeping_service).not_to have_received(:execute)
- end
-
- it 'does not perform housekeeping when project does not have a valid import type' do
- project = create(:project, :import_started, import_type: nil)
-
- project.import_finish
-
- expect(housekeeping_service).not_to have_received(:execute)
- end
- end
- end
-
describe '#latest_successful_builds_for' do
def create_pipeline(status = 'success')
create(:ci_pipeline, project: project,
@@ -1978,6 +1976,42 @@ describe Project do
end
end
+ describe '#import_status' do
+ context 'with import_state' do
+ it 'returns the right status' do
+ project = create(:project, :import_started)
+
+ expect(project.import_status).to eq("started")
+ end
+ end
+
+ context 'without import_state' do
+ it 'returns none' do
+ project = create(:project)
+
+ expect(project.import_status).to eq('none')
+ end
+ end
+ end
+
+ describe '#human_import_status_name' do
+ context 'with import_state' do
+ it 'returns the right human import status' do
+ project = create(:project, :import_started)
+
+ expect(project.human_import_status_name).to eq('started')
+ end
+ end
+
+ context 'without import_state' do
+ it 'returns none' do
+ project = create(:project)
+
+ expect(project.human_import_status_name).to eq('none')
+ end
+ end
+ end
+
describe '#add_import_job' do
let(:import_jid) { '123' }
@@ -2108,6 +2142,39 @@ describe Project do
it 'includes ancestors upto but excluding the given ancestor' do
expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
end
+
+ describe 'with hierarchy_order' do
+ it 'returns ancestors ordered by descending hierarchy' do
+ expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
+ end
+
+ it 'can be used with upto option' do
+ expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
+ end
+ end
+ end
+
+ describe '#root_ancestor' do
+ let(:project) { create(:project) }
+
+ subject { project.root_ancestor }
+
+ it { is_expected.to eq(project.namespace) }
+
+ context 'in a group' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ it { is_expected.to eq(group) }
+ end
+
+ context 'in a nested group', :nested_groups do
+ let(:root) { create(:group) }
+ let(:child) { create(:group, parent: root) }
+ let(:project) { create(:project, group: child) }
+
+ it { is_expected.to eq(root) }
+ end
end
describe '#lfs_enabled?' do
@@ -2720,6 +2787,17 @@ describe Project do
end
end
+ describe '#lfs_http_url_to_repo' do
+ let(:project) { create(:project) }
+
+ it 'returns the url to the repo without a username' do
+ lfs_http_url_to_repo = project.lfs_http_url_to_repo('operation_that_doesnt_matter')
+
+ expect(lfs_http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(lfs_http_url_to_repo).not_to include('@')
+ end
+ end
+
describe '#pipeline_status' do
let(:project) { create(:project, :repository) }
it 'builds a pipeline status' do
@@ -3372,7 +3450,7 @@ describe Project do
context 'with a ref that is not the default branch' do
it 'returns the latest successful pipeline for the given ref' do
- expect(project.pipelines).to receive(:latest_successful_for).with('foo')
+ expect(project.ci_pipelines).to receive(:latest_successful_for).with('foo')
project.latest_successful_pipeline_for('foo')
end
@@ -3400,7 +3478,7 @@ describe Project do
it 'memoizes and returns the latest successful pipeline for the default branch' do
pipeline = double(:pipeline)
- expect(project.pipelines).to receive(:latest_successful_for)
+ expect(project.ci_pipelines).to receive(:latest_successful_for)
.with(project.default_branch)
.and_return(pipeline)
.once
@@ -3414,13 +3492,14 @@ describe Project do
describe '#after_import' do
let(:project) { create(:project) }
+ let(:import_state) { create(:import_state, project: project) }
it 'runs the correct hooks' do
expect(project.repository).to receive(:after_import)
expect(project.wiki.repository).to receive(:after_import)
- expect(project).to receive(:import_finish)
+ expect(import_state).to receive(:finish)
expect(project).to receive(:update_project_counter_caches)
- expect(project).to receive(:remove_import_jid)
+ expect(import_state).to receive(:remove_jid)
expect(project).to receive(:after_create_default_branch)
expect(project).to receive(:refresh_markdown_cache!)
@@ -3430,6 +3509,10 @@ describe Project do
context 'branch protection' do
let(:project) { create(:project, :repository) }
+ before do
+ create(:import_state, :started, project: project)
+ end
+
it 'does not protect when branch protection is disabled' do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
@@ -3485,37 +3568,6 @@ describe Project do
end
end
- describe '#remove_import_jid', :clean_gitlab_redis_cache do
- let(:project) { }
-
- context 'without an import JID' do
- it 'does nothing' do
- project = create(:project)
-
- expect(Gitlab::SidekiqStatus)
- .not_to receive(:unset)
-
- project.remove_import_jid
- end
- end
-
- context 'with an import JID' do
- it 'unsets the import JID' do
- project = create(:project)
- create(:import_state, project: project, jid: '123')
-
- expect(Gitlab::SidekiqStatus)
- .to receive(:unset)
- .with('123')
- .and_call_original
-
- project.remove_import_jid
-
- expect(project.import_jid).to be_nil
- end
- end
- end
-
describe '#wiki_repository_exists?' do
it 'returns true when the wiki repository exists' do
project = create(:project, :wiki_repo)
@@ -3846,7 +3898,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 }
@@ -4019,6 +4071,27 @@ describe Project do
end
end
+ describe '#all_clusters' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) }
+
+ subject { project.all_clusters }
+
+ it 'returns project level cluster' do
+ expect(subject).to eq([cluster])
+ end
+
+ context 'project belongs to a group' do
+ let(:group_cluster) { create(:cluster, :group) }
+ let(:group) { group_cluster.group }
+ let(:project) { create(:project, group: group) }
+
+ it 'returns clusters for groups of this project' do
+ expect(subject).to contain_exactly(cluster, group_cluster)
+ end
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index a83a31ae88c..3692fe9a559 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -6,6 +6,8 @@ describe PrometheusMetric do
subject { build(:prometheus_metric) }
let(:other_project) { build(:project) }
+ it_behaves_like 'having unique enum values'
+
it { is_expected.to belong_to(:project) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:query) }
diff --git a/spec/models/push_event_payload_spec.rb b/spec/models/push_event_payload_spec.rb
index a049ad35584..69a4922b6fd 100644
--- a/spec/models/push_event_payload_spec.rb
+++ b/spec/models/push_event_payload_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe PushEventPayload do
+ it_behaves_like 'having unique enum values'
+
describe 'saving payloads' do
it 'does not allow commit messages longer than 70 characters' do
event = create(:push_event)
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index da61a5f2771..b12ca79847c 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -174,7 +174,15 @@ describe RemoteMirror do
end
context 'with remote mirroring enabled' do
+ it 'defaults to disabling only protected branches' do
+ expect(remote_mirror.only_protected_branches?).to be_falsey
+ end
+
context 'with only protected branches enabled' do
+ before do
+ remote_mirror.only_protected_branches = true
+ end
+
context 'when it did not update in the last minute' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 187283b284b..f09b4b67061 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1488,6 +1488,7 @@ describe Repository do
:size,
:commit_count,
:rendered_readme,
+ :readme_path,
:contribution_guide,
:changelog,
:license_blob,
@@ -1874,6 +1875,42 @@ describe Repository do
end
end
+ describe '#readme_path', :use_clean_rails_memory_store_caching do
+ context 'with a non-existing repository' do
+ let(:project) { create(:project) }
+
+ it 'returns nil' do
+ expect(repository.readme_path).to be_nil
+ end
+ end
+
+ context 'with an existing repository' do
+ context 'when no README exists' do
+ let(:project) { create(:project, :empty_repo) }
+
+ it 'returns nil' do
+ expect(repository.readme_path).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns the README' do
+ expect(repository.readme_path).to eq("README.md")
+ end
+
+ it 'caches the response' do
+ expect(repository).to receive(:readme).and_call_original.once
+
+ 2.times do
+ expect(repository.readme_path).to eq("README.md")
+ end
+ end
+ end
+ end
+ end
+
describe '#expire_statistics_caches' do
it 'expires the caches' do
expect(repository).to receive(:expire_method_caches)
@@ -2042,9 +2079,10 @@ describe Repository do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
- .with(%i(rendered_readme license_blob license_key license))
+ .with(%i(rendered_readme readme_path license_blob license_key license))
expect(repository).to receive(:rendered_readme)
+ expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
expect(repository).to receive(:license)
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index da6e1b5610d..e7e3f7376e6 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe ResourceLabelEvent, type: :model do
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
+ it_behaves_like 'having unique enum values'
+
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:issue) }
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 82ff2a002e0..3682e21ca40 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -236,7 +236,8 @@ describe Todo do
create(:todo, target: create(:merge_request))
- expect(described_class.for_target(todo.target)).to eq([todo])
+ expect(described_class.for_type(Issue.name).for_target(todo.target))
+ .to contain_exactly(todo)
end
end
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_callout_spec.rb b/spec/models/user_callout_spec.rb
index 64ba17c81fe..d54355afe12 100644
--- a/spec/models/user_callout_spec.rb
+++ b/spec/models/user_callout_spec.rb
@@ -3,6 +3,8 @@ require 'rails_helper'
describe UserCallout do
let!(:callout) { create(:user_callout) }
+ it_behaves_like 'having unique enum values'
+
describe 'relationships' do
it { is_expected.to belong_to(:user) }
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 7bd6dccd0ad..ff075e65c76 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4,6 +4,8 @@ describe User do
include ProjectForksHelper
include TermsHelper
+ it_behaves_like 'having unique enum values'
+
describe 'modules' do
subject { described_class }
@@ -2323,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
@@ -3229,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/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index e8096358f7d..7e25c53e77c 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -10,11 +10,50 @@ describe NotePolicy, mdoels: true do
return @policies if @policies
noteable ||= issue
- note = create(:note, noteable: noteable, author: user, project: project)
+ note = if noteable.is_a?(Commit)
+ create(:note_on_commit, commit_id: noteable.id, author: user, project: project)
+ else
+ create(:note, noteable: noteable, author: user, project: project)
+ end
@policies = described_class.new(user, note)
end
+ shared_examples_for 'a discussion with a private noteable' do
+ let(:noteable) { issue }
+ let(:policy) { policies(noteable) }
+
+ context 'when the note author can no longer see the noteable' do
+ it 'can not edit nor read the note' do
+ expect(policy).to be_disallowed(:admin_note)
+ expect(policy).to be_disallowed(:resolve_note)
+ expect(policy).to be_disallowed(:read_note)
+ end
+ end
+
+ context 'when the note author can still see the noteable' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can edit the note' do
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
+ end
+ end
+ end
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ context 'when the noteable is a commit' do
+ it_behaves_like 'a discussion with a private noteable' do
+ let(:noteable) { project.repository.head_commit }
+ end
+ end
+ end
+
context 'when the project is public' do
context 'when the note author is not a project member' do
it 'can edit a note' do
@@ -24,14 +63,48 @@ describe NotePolicy, mdoels: true do
end
end
- context 'when the noteable is a snippet' do
+ context 'when the noteable is a project snippet' do
+ it 'can edit note' do
+ policies = policies(create(:project_snippet, :public, project: project))
+
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+
+ context 'when it is private' do
+ it_behaves_like 'a discussion with a private noteable' do
+ let(:noteable) { create(:project_snippet, :private, project: project) }
+ end
+ end
+ end
+
+ context 'when the noteable is a personal snippet' do
it 'can edit note' do
- policies = policies(create(:project_snippet, project: project))
+ policies = policies(create(:personal_snippet, :public))
expect(policies).to be_allowed(:admin_note)
expect(policies).to be_allowed(:resolve_note)
expect(policies).to be_allowed(:read_note)
end
+
+ context 'when it is private' do
+ it 'can not edit nor read the note' do
+ policies = policies(create(:personal_snippet, :private))
+
+ expect(policies).to be_disallowed(:admin_note)
+ expect(policies).to be_disallowed(:resolve_note)
+ expect(policies).to be_disallowed(:read_note)
+ end
+ end
+ end
+
+ context 'when a discussion is confidential' do
+ before do
+ issue.update_attribute(:confidential, true)
+ end
+
+ it_behaves_like 'a discussion with a private noteable'
end
context 'when a discussion is locked' do
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/applications_spec.rb b/spec/requests/api/applications_spec.rb
index 270e12bf201..6154be5c425 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -25,7 +25,7 @@ describe API::Applications, :api do
it 'does not allow creating an application with the wrong redirect_uri format' do
expect do
- post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: ''
+ post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://', scopes: ''
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(400)
@@ -33,6 +33,16 @@ describe API::Applications, :api do
expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
end
+ it 'does not allow creating an application with a forbidden URI format' do
+ expect do
+ post api('/applications', admin_user), name: 'application_name', redirect_uri: 'javascript://alert()', scopes: ''
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response).to be_a Hash
+ expect(json_response['message']['redirect_uri'][0]).to eq('is forbidden by the server.')
+ end
+
it 'does not allow creating an application without a name' do
expect do
post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: ''
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index cd43bec35df..a43304c9b83 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -16,8 +16,8 @@ describe API::CommitStatuses do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
- let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master', protected: false) }
- let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop', protected: false) }
+ let!(:master) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'master', protected: false) }
+ let!(:develop) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'develop', protected: false) }
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 329d069ef3d..9e599c2175f 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -818,7 +818,7 @@ describe API::Commits do
end
context 'when the ref has a pipeline' do
- let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) }
+ let!(:pipeline) { project.ci_pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) }
it 'includes a "created" status' do
get api(route, current_user)
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 334dbb1c34c..620f9f5e1d6 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -121,6 +121,13 @@ describe API::Files do
end
end
+ context 'when PATs are used' do
+ it_behaves_like 'repository files' do
+ let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
+ let(:current_user) { { personal_access_token: token } }
+ end
+ end
+
context 'when authenticated', 'as a developer' do
it_behaves_like 'repository files' do
let(:current_user) { user }
@@ -217,6 +224,13 @@ describe API::Files do
end
end
+ context 'when PATs are used' do
+ it_behaves_like 'repository files' do
+ let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
+ let(:current_user) { { personal_access_token: token } }
+ end
+ end
+
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
let(:request) { get api(route(file_path)), params }
@@ -317,6 +331,21 @@ describe API::Files do
let(:request) { get api(route(file_path), guest), params }
end
end
+
+ context 'when PATs are used' do
+ it 'returns file by commit sha' do
+ token = create(:personal_access_token, scopes: ['read_repository'], user: user)
+
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(route(file_path) + "/raw", personal_access_token: token), params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
describe "POST /projects/:id/repository/files/:file_path" do
@@ -362,6 +391,24 @@ describe API::Files do
expect(response).to have_gitlab_http_status(400)
end
+ context 'with PATs' do
+ it 'returns 403 with `read_repository` scope' do
+ token = create(:personal_access_token, scopes: ['read_repository'], user: user)
+
+ post api(route(file_path), personal_access_token: token), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'returns 201 with `api` scope' do
+ token = create(:personal_access_token, scopes: ['api'], user: user)
+
+ post api(route(file_path), personal_access_token: token), params
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+
context "when specifying an author" do
it "creates a new file with the specified author" do
params.merge!(author_email: author_email, author_name: author_name)
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index cca449e9e56..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) }
@@ -206,13 +205,33 @@ describe API::Helpers do
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
+
+ context 'when impersonation is disabled' do
+ let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'does not allow impersonation tokens' do
+ expect { current_user }.to raise_error Gitlab::Auth::ImpersonationDisabled
+ end
+ end
end
end
describe '.handle_api_exception' do
before do
- allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
allow_any_instance_of(self.class).to receive(:rack_response)
+ 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
@@ -228,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
@@ -242,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)
@@ -259,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/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/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index e4e0ca285e0..27bcde77860 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -359,6 +359,8 @@ describe API::MergeRequests do
expect(json_response['should_close_merge_request']).to be_falsy
expect(json_response['force_close_merge_request']).to be_falsy
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
+ expect(json_response['merge_error']).to eq(merge_request.merge_error)
+ expect(json_response).not_to include('rebase_in_progress')
end
it 'exposes description and title html when render_html is true' do
@@ -369,6 +371,14 @@ describe API::MergeRequests do
expect(json_response).to include('title_html', 'description_html')
end
+ it 'exposes rebase_in_progress when include_rebase_in_progress is true' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), include_rebase_in_progress: true
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to include('rebase_in_progress')
+ end
+
context 'merge_request_metrics' do
before do
merge_request.metrics.update!(merged_by: user,
@@ -1181,6 +1191,26 @@ describe API::MergeRequests do
end
end
+ describe 'PUT :id/merge_requests/:merge_request_iid/rebase' do
+ it 'enqueues a rebase of the merge request against the target branch' do
+ Sidekiq::Testing.fake! do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
+ end
+
+ expect(response).to have_gitlab_http_status(202)
+ expect(RebaseWorker.jobs.size).to eq(1)
+ end
+
+ it 'returns 403 if the user cannot push to the branch' do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", guest)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
describe 'Time tracking' do
let(:issuable) { merge_request }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index e2000ab42e8..145356c4df5 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Namespaces do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
- let!(:group1) { create(:group) }
+ let!(:group1) { create(:group, name: 'group.one') }
let!(:group2) { create(:group, :nested) }
describe "GET /namespaces" do
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 638cc9767d4..2e4fa0f9e16 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -304,7 +304,7 @@ describe API::Pipelines do
it 'creates and returns a new pipeline' do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
- end.to change { project.pipelines.count }.by(1)
+ end.to change { project.ci_pipelines.count }.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a Hash
@@ -317,8 +317,8 @@ describe API::Pipelines do
it 'creates and returns a new pipeline using the given variables' do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables
- end.to change { project.pipelines.count }.by(1)
- expect_variables(project.pipelines.last.variables, variables)
+ end.to change { project.ci_pipelines.count }.by(1)
+ expect_variables(project.ci_pipelines.last.variables, variables)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a Hash
@@ -338,8 +338,8 @@ describe API::Pipelines do
it 'creates and returns a new pipeline using the given variables' do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables
- end.to change { project.pipelines.count }.by(1)
- expect_variables(project.pipelines.last.variables, variables)
+ end.to change { project.ci_pipelines.count }.by(1)
+ expect_variables(project.ci_pipelines.last.variables, variables)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to be_a Hash
@@ -353,7 +353,7 @@ describe API::Pipelines do
it "doesn't create a job" do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
- end.not_to change { project.pipelines.count }
+ end.not_to change { project.ci_pipelines.count }
expect(response).to have_gitlab_http_status(400)
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index c8fa4754810..204702b8a5a 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -42,7 +42,7 @@ describe API::ProjectImport do
end
it 'does not schedule an import for a namespace that does not exist' do
- expect_any_instance_of(Project).not_to receive(:import_schedule)
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
expect(::Projects::CreateService).not_to receive(:new)
post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file)
@@ -52,7 +52,7 @@ describe API::ProjectImport do
end
it 'does not schedule an import if the user has no permission to the namespace' do
- expect_any_instance_of(Project).not_to receive(:import_schedule)
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
post(api('/projects/import', create(:user)),
path: 'test-import3',
@@ -64,7 +64,7 @@ describe API::ProjectImport do
end
it 'does not schedule an import if the user uploads no valid file' do
- expect_any_instance_of(Project).not_to receive(:import_schedule)
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
post api('/projects/import', user), path: 'test-import3', file: './random/test'
@@ -119,7 +119,7 @@ describe API::ProjectImport do
let(:existing_project) { create(:project, namespace: user.namespace) }
it 'does not schedule an import' do
- expect_any_instance_of(Project).not_to receive(:import_schedule)
+ expect_any_instance_of(ProjectImportState).not_to receive(:schedule)
post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file)
@@ -139,7 +139,7 @@ describe API::ProjectImport do
end
def stub_import(namespace)
- expect_any_instance_of(Project).to receive(:import_schedule)
+ expect_any_instance_of(ProjectImportState).to receive(:schedule)
expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original
end
end
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/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 0ae6796d1e4..658df6945d2 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -39,7 +39,7 @@ describe API::Triggers do
end
context 'Have a commit' do
- let(:pipeline) { project.pipelines.last }
+ let(:pipeline) { project.ci_pipelines.last }
it 'creates pipeline' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c71eae9164a..0dc459d9b5a 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -302,7 +302,7 @@ describe 'Git HTTP requests' do
it 'rejects pushes with 403 Forbidden' do
upload(path, env) do |response|
expect(response).to have_gitlab_http_status(:forbidden)
- expect(response.body).to eq(change_access_error(:push_code))
+ expect(response.body).to eq('You are not allowed to push code to this project.')
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index bdfb12dc5df..5c3b37ef11c 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -36,36 +36,33 @@ describe 'project routing' do
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
let(:controller_path) { controller }
- let(:id) { { id: '1' } }
- let(:format) { {} } # response format, e.g. { format: :html }
- let(:params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", params) if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", params) if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", params) if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", params.merge(**id, **format)) if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", params.merge(**id, **format)) if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", params.merge(id)) if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", params.merge(**id, **format)) if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
end
end
@@ -154,13 +151,12 @@ describe 'project routing' do
end
it 'to #history' do
- expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: :html)
+ expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it_behaves_like 'RESTful project resources' do
let(:actions) { [:create, :edit, :show, :destroy] }
let(:controller) { 'wikis' }
- let(:format) { { format: :html } }
end
end
diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb
index 8f795bb561e..c348fc0efac 100644
--- a/spec/rubocop/cop/migration/add_reference_spec.rb
+++ b/spec/rubocop/cop/migration/add_reference_spec.rb
@@ -29,7 +29,7 @@ describe RuboCop::Cop::Migration::AddReference do
expect_offense(<<~RUBY)
call do
add_reference(:projects, :users)
- ^^^^^^^^^^^^^ `add_reference` requires `index: true`
+ ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }`
end
RUBY
end
@@ -38,7 +38,7 @@ describe RuboCop::Cop::Migration::AddReference do
expect_offense(<<~RUBY)
def up
add_reference(:projects, :users, index: false)
- ^^^^^^^^^^^^^ `add_reference` requires `index: true`
+ ^^^^^^^^^^^^^ `add_reference` requires `index: true` or `index: { options... }`
end
RUBY
end
@@ -50,5 +50,13 @@ describe RuboCop::Cop::Migration::AddReference do
end
RUBY
end
+
+ it 'does not register an offense when the index is unique' do
+ expect_no_offenses(<<~RUBY)
+ def up
+ add_reference(:projects, :users, index: { unique: true } )
+ end
+ RUBY
+ end
end
end
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/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
new file mode 100644
index 00000000000..06d9d3657e6
--- /dev/null
+++ b/spec/serializers/issue_board_entity_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssueBoardEntity do
+ let(:project) { create(:project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'has basic attributes' do
+ expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position,
+ :project, :labels)
+ end
+
+ it 'has path and endpoints' do
+ expect(subject).to include(:reference_path, :real_path, :issue_sidebar_endpoint,
+ :toggle_subscription_endpoint, :assignable_labels_endpoint)
+ end
+end
diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb
index 75578816e75..e8c46c0cdee 100644
--- a/spec/serializers/issue_serializer_spec.rb
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -24,4 +24,12 @@ describe IssueSerializer do
expect(json_entity).to match_schema('entities/issue_sidebar')
end
end
+
+ context 'board issue serialization' do
+ let(:serializer) { 'board' }
+
+ it 'matches board issue json schema' do
+ expect(json_entity).to match_schema('entities/issue_board')
+ end
+ end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index e67d12b7a89..774486dcb6d 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -44,7 +44,7 @@ describe PipelineEntity do
expect(subject).to include :flags
expect(subject[:flags])
.to include :latest, :stuck, :auto_devops,
- :yaml_errors, :retryable, :cancelable
+ :yaml_errors, :retryable, :cancelable, :merge_request
end
end
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
new file mode 100644
index 00000000000..8e9cb65f3bc
--- /dev/null
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Ci::ArchiveTraceService, '#execute' do
+ subject { described_class.new.execute(job) }
+
+ context 'when job is finished' do
+ let(:job) { create(:ci_build, :success, :trace_live) }
+
+ it 'creates an archived trace' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_exist
+ end
+
+ context 'when trace is already archived' do
+ let!(:job) { create(:ci_build, :success, :trace_artifact) }
+
+ it 'ignores an exception' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'does not create an archived trace' do
+ expect { subject }.not_to change { Ci::JobArtifact.trace.count }
+ end
+ end
+ end
+
+ context 'when job is running' do
+ let(:job) { create(:ci_build, :running, :trace_live) }
+
+ it 'increments Prometheus counter, sends crash report to Sentry and ignore an error for continuing to archive' do
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(::Gitlab::Ci::Trace::ArchiveError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502',
+ extra: { job_id: job.id } ).once
+
+ expect(Rails.logger)
+ .to receive(:error)
+ .with("Failed to archive trace. id: #{job.id} message: Job is not finished yet")
+ .and_call_original
+
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:job_trace_archive_failed_total, "Counter of failed attempts of trace archiving")
+ .and_call_original
+
+ expect { subject }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4d9c5aabbda..ccc6b0ef1c7 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -18,7 +18,8 @@ describe Ci::CreatePipelineService do
message: 'Message',
ref: ref_name,
trigger_request: nil,
- variables_attributes: nil)
+ variables_attributes: nil,
+ merge_request: nil)
params = { ref: ref,
before: '00000000',
after: after,
@@ -26,7 +27,7 @@ describe Ci::CreatePipelineService do
variables_attributes: variables_attributes }
described_class.new(project, user, params).execute(
- source, trigger_request: trigger_request)
+ source, trigger_request: trigger_request, merge_request: merge_request)
end
context 'valid params' do
@@ -43,7 +44,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_valid
expect(pipeline).to be_persisted
expect(pipeline).to be_push
- expect(pipeline).to eq(project.pipelines.last)
+ expect(pipeline).to eq(project.ci_pipelines.last)
expect(pipeline).to have_attributes(user: user)
expect(pipeline).to have_attributes(status: 'pending')
expect(pipeline.repository_source?).to be true
@@ -60,10 +61,10 @@ describe Ci::CreatePipelineService do
context 'when merge requests already exist for this source branch' do
let(:merge_request_1) do
- create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
+ create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project)
end
let(:merge_request_2) do
- create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
+ create(:merge_request, source_branch: 'feature', target_branch: "v1.1.0", source_project: project)
end
context 'when related merge request is already merged' do
@@ -83,7 +84,7 @@ describe Ci::CreatePipelineService do
merge_request_1
merge_request_2
- head_pipeline = execute_service
+ head_pipeline = execute_service(ref: 'feature', after: nil)
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
@@ -123,12 +124,12 @@ describe Ci::CreatePipelineService do
let!(:target_project) { create(:project, :repository) }
it 'updates head pipeline for merge request' do
- merge_request = create(:merge_request, source_branch: 'master',
- target_branch: "branch_1",
+ merge_request = create(:merge_request, source_branch: 'feature',
+ target_branch: "master",
source_project: project,
target_project: target_project)
- head_pipeline = execute_service
+ head_pipeline = execute_service(ref: 'feature', after: nil)
expect(merge_request.reload.head_pipeline).to eq(head_pipeline)
end
@@ -656,5 +657,244 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ describe 'Merge request pipelines' do
+ let(:pipeline) do
+ execute_service(source: source, merge_request: merge_request, ref: ref_name)
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:ref_name) { 'feature' }
+
+ context 'when source is merge request' do
+ let(:source) { :merge_request }
+
+ context "when config has merge_requests keywords" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['merge_requests']
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo',
+ except: ['merge_requests']
+ }
+ }
+ 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 'creates a merge request pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_merge_request
+ expect(pipeline.merge_request).to eq(merge_request)
+ expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test])
+ end
+
+ context 'when ref is tag' do
+ let(:ref_name) { 'v1.1.0' }
+
+ it 'does not create a merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:tag]).to eq(["is not included in the list"])
+ end
+ end
+
+ context 'when merge request is created from a forked project' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: ref_name,
+ target_project: target_project,
+ target_branch: 'master')
+ end
+
+ let!(:project) { fork_project(target_project, nil, repository: true) }
+ let!(:target_project) { create(:project, :repository) }
+
+ it 'creates a merge request pipeline in the forked project' do
+ expect(pipeline).to be_persisted
+ expect(project.ci_pipelines).to eq([pipeline])
+ expect(target_project.ci_pipelines).to be_empty
+ end
+ end
+
+ context "when there are no matched jobs" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo',
+ except: ['merge_requests']
+ }
+ }
+ 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 merge request is not specified' do
+ let(:merge_request) { nil }
+
+ it 'does not create a merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:merge_request]).to eq(["can't be blank"])
+ end
+ end
+ end
+
+ context "when config does not have merge_requests keywords" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo'
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo'
+ }
+ }
+ 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
+
+ context 'when merge request is not specified' do
+ let(:merge_request) { nil }
+
+ 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
+ let(:source) { :web }
+
+ context "when config has merge_requests keywords" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['merge_requests']
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo',
+ except: ['merge_requests']
+ }
+ }
+ 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[:merge_request]).to eq(["must be blank"])
+ end
+ end
+
+ context 'when merge request is not specified' do
+ let(:merge_request) { nil }
+
+ it 'creates a branch pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_web
+ expect(pipeline.merge_request).to be_nil
+ expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[build pages])
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#execute!' do
+ subject { service.execute!(*args) }
+
+ let(:service) { described_class.new(project, user, ref: ref_name) }
+ let(:args) { [:push] }
+
+ context 'when user has a permission to create a pipeline' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'creates a pipeline' do
+ expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ end
+ end
+
+ context 'when user does not have a permission to create a pipeline' do
+ let(:user) { create(:user) }
+
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(described_class::CreateError)
+ .with_message('Insufficient permissions to create a new pipeline')
+ end
+ end
end
end
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/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index efee158739d..d69678c1277 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -19,6 +19,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
subject { described_class.new.execute(provider) }
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ end
+
shared_examples 'success' do
it 'configures provider and kubernetes' do
subject
@@ -39,14 +43,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
expect(platform.token).to eq(token)
end
- it 'creates kubernetes namespace model' do
- subject
+ it 'calls ClusterPlatformConfigureWorker in a ascync fashion' do
+ expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id)
- kubernetes_namespace = cluster.reload.kubernetes_namespace
- expect(kubernetes_namespace).to be_persisted
- expect(kubernetes_namespace.namespace).to eq(namespace)
- expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
- expect(kubernetes_namespace.service_account_token).to be_present
+ subject
end
end
@@ -104,8 +104,10 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url)
stub_kubeclient_create_namespace(api_url)
+ stub_kubeclient_get_service_account_error(api_url, 'gitlab')
stub_kubeclient_create_service_account(api_url)
stub_kubeclient_create_secret(api_url)
+ stub_kubeclient_put_secret(api_url, 'gitlab-token')
stub_kubeclient_get_secret(
api_url,
@@ -115,19 +117,6 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
namespace: 'default'
}
)
-
- stub_kubeclient_get_namespace(api_url, namespace: namespace)
- stub_kubeclient_create_service_account(api_url, namespace: namespace)
- stub_kubeclient_create_secret(api_url, namespace: namespace)
-
- stub_kubeclient_get_secret(
- api_url,
- {
- metadata_name: "#{namespace}-token",
- token: Base64.encode64(token),
- namespace: namespace
- }
- )
end
end
@@ -155,8 +144,8 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
before do
provider.legacy_abac = false
+ stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
stub_kubeclient_create_cluster_role_binding(api_url)
- stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end
include_context 'kubernetes information successfully fetched'
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
index 661364ac765..fe785735fef 100644
--- a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
@@ -10,6 +10,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
let(:api_url) { 'https://kubernetes.example.com' }
let(:project) { cluster.project }
let(:cluster_project) { cluster.cluster_project }
+ let(:namespace) { "#{project.path}-#{project.id}" }
subject do
described_class.new(
@@ -18,40 +19,31 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
).execute
end
- shared_context 'kubernetes requests' do
- before do
- stub_kubeclient_discover(api_url)
- stub_kubeclient_get_namespace(api_url)
- stub_kubeclient_create_service_account(api_url)
- stub_kubeclient_create_secret(api_url)
-
- stub_kubeclient_get_namespace(api_url, namespace: namespace)
- stub_kubeclient_create_service_account(api_url, namespace: namespace)
- stub_kubeclient_create_secret(api_url, namespace: namespace)
-
- stub_kubeclient_get_secret(
- api_url,
- {
- metadata_name: "#{namespace}-token",
- token: Base64.encode64('sample-token'),
- namespace: namespace
- }
- )
- end
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_get_service_account_error(api_url, 'gitlab')
+ stub_kubeclient_create_service_account(api_url)
+ stub_kubeclient_get_secret_error(api_url, 'gitlab-token')
+ stub_kubeclient_create_secret(api_url)
+
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+ stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: "#{namespace}-token",
+ token: Base64.encode64('sample-token'),
+ namespace: namespace
+ }
+ )
end
- context 'when kubernetes namespace is not persisted' do
- let(:namespace) { "#{project.path}-#{project.id}" }
-
- let(:kubernetes_namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
- end
-
- include_context 'kubernetes requests'
-
+ shared_examples 'successful creation of kubernetes namespace' do
it 'creates a Clusters::KubernetesNamespace' do
expect do
subject
@@ -59,7 +51,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
end
it 'creates project service account' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once
subject
end
@@ -74,42 +66,69 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
end
end
- context 'when there is a Kubernetes Namespace associated' do
- let(:namespace) { 'new-namespace' }
+ context 'group clusters' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, group: group) }
+
+ context 'when kubernetes namespace is not persisted' do
+ let(:kubernetes_namespace) do
+ build(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: project)
+ end
- let(:kubernetes_namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
+ it_behaves_like 'successful creation of kubernetes namespace'
end
+ end
- include_context 'kubernetes requests'
+ context 'project clusters' do
+ context 'when kubernetes namespace is not persisted' do
+ let(:kubernetes_namespace) do
+ build(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
- before do
- platform.update_column(:namespace, 'new-namespace')
+ it_behaves_like 'successful creation of kubernetes namespace'
end
- it 'does not create any Clusters::KubernetesNamespace' do
- subject
+ context 'when there is a Kubernetes Namespace associated' do
+ let(:namespace) { 'new-namespace' }
- expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace)
- end
+ let(:kubernetes_namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
- it 'creates project service account' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+ before do
+ platform.update_column(:namespace, 'new-namespace')
+ end
- subject
- end
+ it 'does not create any Clusters::KubernetesNamespace' do
+ subject
- it 'updates Clusters::KubernetesNamespace' do
- subject
+ expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace)
+ end
- kubernetes_namespace.reload
+ it 'creates project service account' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:execute).once
- expect(kubernetes_namespace.namespace).to eq(namespace)
- expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
- expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ subject
+ end
+
+ it 'updates Clusters::KubernetesNamespace' do
+ subject
+
+ kubernetes_namespace.reload
+
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ end
end
end
end
diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
index 588edff85d4..11a65d0c300 100644
--- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
+describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do
include KubernetesHelpers
let(:api_url) { 'http://111.111.111.111' }
@@ -55,7 +55,11 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
before do
stub_kubeclient_discover(api_url)
stub_kubeclient_get_namespace(api_url, namespace: namespace)
- stub_kubeclient_create_service_account(api_url, namespace: namespace )
+
+ stub_kubeclient_get_service_account_error(api_url, service_account_name, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+
+ stub_kubeclient_get_secret_error(api_url, token_name, namespace: namespace)
stub_kubeclient_create_secret(api_url, namespace: namespace)
end
@@ -74,10 +78,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
context 'with RBAC cluster' do
let(:rbac) { true }
+ let(:cluster_role_binding_name) { 'gitlab-admin' }
before do
cluster.platform_kubernetes.rbac!
+ stub_kubeclient_get_cluster_role_binding_error(api_url, cluster_role_binding_name)
stub_kubeclient_create_cluster_role_binding(api_url)
end
@@ -130,10 +136,12 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
context 'With RBAC enabled cluster' do
let(:rbac) { true }
+ let(:role_binding_name) { "gitlab-#{namespace}"}
before do
cluster.platform_kubernetes.rbac!
+ stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
new file mode 100644
index 00000000000..58ab3c3cf73
--- /dev/null
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::RefreshService do
+ shared_examples 'creates a kubernetes namespace' do
+ let(:token) { 'aaaaaa' }
+ let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+ let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+ it 'creates a kubernetes namespace' do
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+ expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+
+ expect { subject }.to change(project.kubernetes_namespaces, :count)
+
+ kubernetes_namespace = cluster.kubernetes_namespaces.first
+ expect(kubernetes_namespace).to be_present
+ expect(kubernetes_namespace.project).to eq(project)
+ end
+ end
+
+ shared_examples 'does not create a kubernetes namespace' do
+ it 'does not create a new kubernetes namespace' do
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).not_to receive(:namespace_creator)
+ expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).not_to receive(:new)
+
+ expect { subject }.not_to change(Clusters::KubernetesNamespace, :count)
+ end
+ end
+
+ describe '.create_or_update_namespaces_for_cluster' do
+ let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:project) { cluster.project }
+
+ subject { described_class.create_or_update_namespaces_for_cluster(cluster) }
+
+ context 'cluster is project level' do
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+
+ context 'cluster is group level' do
+ let(:cluster) { create(:cluster, :provided_by_user, :group) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, group: group) }
+
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+ end
+
+ describe '.create_or_update_namespaces_for_project' do
+ let(:project) { create(:project) }
+
+ subject { described_class.create_or_update_namespaces_for_project(project) }
+
+ it 'creates no kubernetes namespaces' do
+ expect { subject }.not_to change(project.kubernetes_namespaces, :count)
+ end
+
+ context 'project has a project cluster' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :project_type, projects: [project]) }
+
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+
+ context 'project belongs to a group cluster' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ let(:group) { cluster.group }
+ let(:project) { create(:project, group: group) }
+
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+ end
+end
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
index 5f3c8e82715..84c48d63c64 100644
--- a/spec/services/files/multi_service_spec.rb
+++ b/spec/services/files/multi_service_spec.rb
@@ -122,26 +122,47 @@ describe Files::MultiService do
let(:action) { 'move' }
let(:new_file_path) { 'files/ruby/new_popen.rb' }
+ let(:result) { subject.execute }
+ let(:blob) { repository.blob_at_branch(branch_name, new_file_path) }
+
context 'when original file has been updated' do
before do
update_file(original_file_path)
end
it 'rejects the commit' do
- results = subject.execute
-
- expect(results[:status]).to eq(:error)
- expect(results[:message]).to match(original_file_path)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to match(original_file_path)
end
end
- context 'when original file have not been updated' do
+ context 'when original file has not been updated' do
it 'moves the file' do
- results = subject.execute
- blob = project.repository.blob_at_branch(branch_name, new_file_path)
-
- expect(results[:status]).to eq(:success)
+ expect(result[:status]).to eq(:success)
expect(blob).to be_present
+ expect(blob.data).to eq(file_content)
+ end
+
+ context 'when content is nil' do
+ let(:file_content) { nil }
+
+ it 'moves the existing content untouched' do
+ original_content = repository.blob_at_branch(branch_name, original_file_path).data
+
+ expect(result[:status]).to eq(:success)
+ expect(blob).to be_present
+ expect(blob.data).to eq(original_content)
+ end
+ end
+
+ context 'when content is an empty string' do
+ let(:file_content) { '' }
+
+ it 'moves the file and empties it' do
+ expect(result[:status]).to eq(:success)
+ expect(blob).not_to be_nil
+ expect(blob.data).to eq('')
+ end
end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 74bcc15f912..5a3ecb1019b 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -159,6 +159,78 @@ describe MergeRequests::CreateService do
end
end
end
+
+ describe 'Merge request pipelines' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ context "when .gitlab-ci.yml has merge_requests keywords" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['merge_requests']
+ }
+ }
+ end
+
+ it 'creates a merge request pipeline and sets it as a head pipeline' do
+ expect(merge_request).to be_persisted
+
+ merge_request.reload
+ expect(merge_request.merge_request_pipelines.count).to eq(1)
+ expect(merge_request.actual_head_pipeline).to be_merge_request
+ end
+
+ context "when branch pipeline was created before a merge request pipline has been created" do
+ before do
+ create(:ci_pipeline, project: merge_request.source_project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ tag: false)
+
+ merge_request
+ end
+
+ it 'sets the latest merge request pipeline as the head pipeline' do
+ expect(merge_request.actual_head_pipeline).to be_merge_request
+ end
+ end
+
+ context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
+ before do
+ stub_feature_flags(ci_merge_request_pipeline: false)
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect(merge_request).to be_persisted
+
+ merge_request.reload
+ expect(merge_request.merge_request_pipelines.count).to eq(0)
+ end
+ end
+ end
+
+ context "when .gitlab-ci.yml does not have merge_requests keywords" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo'
+ }
+ }
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect(merge_request).to be_persisted
+
+ merge_request.reload
+ expect(merge_request.merge_request_pipelines.count).to eq(0)
+ end
+ end
+ end
end
it_behaves_like 'new issuable record that supports quick actions' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 61c6ba7d550..1d9c75dedce 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -132,6 +132,94 @@ describe MergeRequests::RefreshService do
end
end
+ describe 'Merge request pipelines' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+
+ context "when .gitlab-ci.yml has merge_requests keywords" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['merge_requests']
+ }
+ }
+ end
+
+ it 'create merge request pipeline' do
+ expect { subject }
+ .to change { @merge_request.merge_request_pipelines.count }.by(1)
+ .and change { @fork_merge_request.merge_request_pipelines.count }.by(1)
+ .and change { @another_merge_request.merge_request_pipelines.count }.by(1)
+ end
+
+ context "when branch pipeline was created before a merge request pipline has been created" do
+ before do
+ create(:ci_pipeline, project: @merge_request.source_project,
+ sha: @merge_request.diff_head_sha,
+ ref: @merge_request.source_branch,
+ tag: false)
+
+ subject
+ end
+
+ it 'sets the latest merge request pipeline as a head pipeline' do
+ @merge_request.reload
+ expect(@merge_request.actual_head_pipeline).to be_merge_request
+ end
+
+ it 'returns pipelines in correct order' do
+ @merge_request.reload
+ expect(@merge_request.all_pipelines.first).to be_merge_request
+ expect(@merge_request.all_pipelines.second).to be_push
+ end
+ end
+
+ context "when MergeRequestUpdateWorker is retried by an exception" do
+ it 'does not re-create a duplicate merge request pipeline' do
+ expect do
+ service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
+ end.to change { @merge_request.merge_request_pipelines.count }.by(1)
+
+ expect do
+ service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
+ end.not_to change { @merge_request.merge_request_pipelines.count }
+ end
+ end
+
+ context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
+ before do
+ stub_feature_flags(ci_merge_request_pipeline: false)
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect { subject }
+ .not_to change { @merge_request.merge_request_pipelines.count }
+ end
+ end
+ end
+
+ context "when .gitlab-ci.yml does not have merge_requests keywords" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo'
+ }
+ }
+ end
+
+ it 'does not create a merge request pipeline' do
+ expect { subject }
+ .not_to change { @merge_request.merge_request_pipelines.count }
+ end
+ end
+ end
+
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
let(:notification_service) { spy('notification_service') }
@@ -533,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..0f6c2604984 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2146,6 +2146,27 @@ 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
+
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/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index 141ccf7c4d8..da078dd36c6 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -47,7 +47,7 @@ describe Projects::CreateFromTemplateService do
end
it 'is not scheduled' do
- expect(project.import_scheduled?).to be(false)
+ expect(project.import_scheduled?).to be_nil
end
it 'repository is empty' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 08de27ca44a..f71e2b4bc24 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -261,6 +261,32 @@ describe Projects::CreateService, '#execute' do
end
end
+ context 'when group has kubernetes cluster' do
+ let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { group_cluster.group }
+
+ let(:token) { 'aaaa' }
+ let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+ let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+ before do
+ group.add_owner(user)
+
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+ expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+ end
+
+ it 'creates kubernetes namespace for the project' do
+ project = create_project(user, opts.merge!(namespace_id: group.id))
+
+ expect(project).to be_valid
+
+ kubernetes_namespace = group_cluster.kubernetes_namespaces.first
+ expect(kubernetes_namespace).to be_present
+ expect(kubernetes_namespace.project).to eq(project)
+ end
+ end
+
context 'when there is an active service template' do
before do
create(:service, project: nil, template: true, active: true)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2e07d4f8013..132ad9a2646 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -62,6 +62,32 @@ describe Projects::TransferService do
expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
end
+
+ context 'new group has a kubernetes cluster' do
+ let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { group_cluster.group }
+
+ let(:token) { 'aaaa' }
+ let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
+ let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
+
+ subject { transfer_project(project, user, group) }
+
+ before do
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
+ expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
+ end
+
+ it 'creates kubernetes namespace for the project' do
+ subject
+
+ expect(project.kubernetes_namespaces.count).to eq(1)
+
+ kubernetes_namespace = group_cluster.kubernetes_namespaces.first
+ expect(kubernetes_namespace).to be_present
+ expect(kubernetes_namespace.project).to eq(project)
+ end
+ end
end
context 'when transfer fails' do
diff --git a/spec/support/active_record_enum.rb b/spec/support/active_record_enum.rb
new file mode 100644
index 00000000000..fb1189c7f17
--- /dev/null
+++ b/spec/support/active_record_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+shared_examples 'having unique enum values' do
+ described_class.defined_enums.each do |name, enum|
+ it "has unique values in #{name.inspect}" do
+ duplicated = enum.group_by(&:last).select { |key, value| value.size > 1 }
+
+ expect(duplicated).to be_empty,
+ "Duplicated values detected: #{duplicated.values.map(&Hash.method(:[]))}"
+ end
+ end
+end
diff --git a/spec/support/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb
new file mode 100644
index 00000000000..7e4958f177a
--- /dev/null
+++ b/spec/support/controllers/sessionless_auth_controller_shared_examples.rb
@@ -0,0 +1,92 @@
+shared_examples 'authenticates sessionless user' do |path, format, params|
+ params ||= {}
+
+ before do
+ stub_authentication_activity_metrics(debug: false)
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:default_params) { { format: format }.merge(params.except(:public) || {}) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it 'logs the user in' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_session_override_counter)
+ .and increment(:user_sessionless_authentication_counter)
+
+ get path, default_params.merge(private_token: personal_access_token.token)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(controller.current_user).to eq(user)
+ end
+
+ it 'does not log the user in if page is public', if: params[:public] do
+ get path, default_params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(controller.current_user).to be_nil
+ end
+ end
+
+ context 'when the personal access token has no api scope', unless: params[:public] do
+ it 'does not log the user in' do
+ expect(authentication_metrics)
+ .to increment(:user_unauthenticated_counter)
+
+ personal_access_token.update(scopes: [:read_user])
+
+ get path, default_params.merge(private_token: personal_access_token.token)
+
+ expect(response).not_to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it 'logs the user in' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_session_override_counter)
+ .and increment(:user_sessionless_authentication_counter)
+
+ @request.headers['PRIVATE-TOKEN'] = personal_access_token.token
+ get path, default_params
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the 'feed_token' param is populated with the feed token", if: format == :rss do
+ it "logs the user in" do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_session_override_counter)
+ .and increment(:user_sessionless_authentication_counter)
+
+ get path, default_params.merge(feed_token: user.feed_token)
+
+ expect(response).to have_gitlab_http_status 200
+ end
+ end
+
+ context "when the 'feed_token' param is populated with an invalid feed token", if: format == :rss, unless: params[:public] do
+ it "logs the user" do
+ expect(authentication_metrics)
+ .to increment(:user_unauthenticated_counter)
+
+ get path, default_params.merge(feed_token: 'token')
+
+ expect(response.status).not_to eq 200
+ end
+ end
+
+ it "doesn't log the user in otherwise", unless: params[:public] do
+ expect(authentication_metrics)
+ .to increment(:user_unauthenticated_counter)
+
+ get path, default_params.merge(private_token: 'token')
+
+ expect(response.status).not_to eq(200)
+ end
+end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 18cf08f0b9e..922f3df144d 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -142,6 +142,14 @@ shared_examples 'discussion comments' do |resource_name|
find(comments_selector, match: :first)
end
+ def submit_reply(text)
+ find("#{comments_selector} .js-vue-discussion-reply").click
+ find("#{comments_selector} .note-textarea").send_keys(text)
+
+ click_button "Comment"
+ wait_for_requests
+ end
+
it 'clicking "Start discussion" will post a discussion' do
new_comment = all(comments_selector).last
@@ -149,16 +157,29 @@ shared_examples 'discussion comments' do |resource_name|
expect(new_comment).to have_selector '.discussion'
end
+ if resource_name =~ /(issue|merge request)/
+ it 'can be replied to' do
+ submit_reply('some text')
+
+ expect(page).to have_css('.discussion-notes .note', count: 2)
+ expect(page).to have_content 'Collapse replies'
+ end
+
+ it 'can be collapsed' do
+ submit_reply('another text')
+
+ find('.js-collapse-replies').click
+ expect(page).to have_css('.discussion-notes .note', count: 1)
+ expect(page).to have_content '1 reply'
+ end
+ end
+
if resource_name == 'merge request'
let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] }
it 'shows resolved discussion when toggled' do
- find("#{comments_selector} .js-vue-discussion-reply").click
- find("#{comments_selector} .note-textarea").send_keys('a')
-
- click_button "Comment"
- wait_for_requests
+ submit_reply('a')
click_button "Resolve discussion"
wait_for_requests
diff --git a/spec/support/helpers/features/list_rows_helpers.rb b/spec/support/helpers/features/list_rows_helpers.rb
new file mode 100644
index 00000000000..0626415361c
--- /dev/null
+++ b/spec/support/helpers/features/list_rows_helpers.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+# These helpers allow you to access rows in the list
+#
+# Usage:
+# describe "..." do
+# include Spec::Support::Helpers::Features::ListRowsHelpers
+# ...
+#
+# expect(first_row.text).to include("John Doe")
+# expect(second_row.text).to include("John Smith")
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module ListRowsHelpers
+ def first_row
+ page.all('ul.content-list > li')[0]
+ end
+
+ def second_row
+ page.all('ul.content-list > li')[1]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index ad0053ec5cf..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('button.dropdown-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/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
index b8289e6c5f1..9a5845af90c 100644
--- a/spec/support/helpers/git_http_helpers.rb
+++ b/spec/support/helpers/git_http_helpers.rb
@@ -60,9 +60,4 @@ module GitHttpHelpers
message = Gitlab::GitAccessWiki::ERROR_MESSAGES[error_key]
message || raise("GitAccessWiki error message key '#{error_key}' not found")
end
-
- def change_access_error(error_key)
- message = Gitlab::Checks::ChangeAccess::ERROR_MESSAGES[error_key]
- message || raise("ChangeAccess error message key '#{error_key}' not found")
- end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index ccaf86aa3a6..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"
@@ -47,6 +58,11 @@ module KubernetesHelpers
.to_return(status: [status, "Internal Server Error"])
end
+ def stub_kubeclient_get_service_account_error(api_url, name, namespace: 'default', status: 404)
+ WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts/#{name}")
+ .to_return(status: [status, "Internal Server Error"])
+ end
+
def stub_kubeclient_create_service_account(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts")
.to_return(kube_response({}))
@@ -62,11 +78,26 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
+ def stub_kubeclient_put_secret(api_url, name, namespace: 'default')
+ WebMock.stub_request(:put, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{name}")
+ .to_return(kube_response({}))
+ end
+
+ def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404)
+ WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
+ .to_return(status: [status, "Internal Server Error"])
+ end
+
def stub_kubeclient_create_cluster_role_binding(api_url)
WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings')
.to_return(kube_response({}))
end
+ def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404)
+ WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
+ .to_return(status: [status, "Internal Server Error"])
+ end
+
def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
.to_return(kube_response({}))
@@ -161,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)
@@ -204,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/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 4212be2cc88..ce1f9fce10d 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -49,11 +49,11 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/series?#{query}"
end
- def stub_prometheus_request(url, body: {}, status: 200)
+ def stub_prometheus_request(url, body: {}, status: 200, headers: {})
WebMock.stub_request(:get, url)
.to_return({
status: status,
- headers: { 'Content-Type' => 'application/json' },
+ headers: { 'Content-Type' => 'application/json' }.merge(headers),
body: body.to_json
})
end
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
index 9496a94d8f4..e505a6b7258 100644
--- a/spec/support/helpers/sorting_helper.rb
+++ b/spec/support/helpers/sorting_helper.rb
@@ -10,7 +10,7 @@
#
module SortingHelper
def sorting_by(value)
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link value
end
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index 776119564ec..2851cd9733c 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -27,6 +27,11 @@ module StubConfiguration
allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages))
end
+ def stub_default_url_options(host: "localhost", protocol: "http")
+ url_options = { host: host, protocol: protocol }
+ allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options)
+ end
+
def stub_gravatar_setting(messages)
allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages))
end
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_contexts/change_access_checks_shared_context.rb b/spec/support/shared_contexts/change_access_checks_shared_context.rb
new file mode 100644
index 00000000000..aca18b0c73b
--- /dev/null
+++ b/spec/support/shared_contexts/change_access_checks_shared_context.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_context 'change access checks context' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:ref) { 'refs/heads/master' }
+ let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
+ let(:protocol) { 'ssh' }
+ let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
+ let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
+ let(:change_access) do
+ Gitlab::Checks::ChangeAccess.new(
+ changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger
+ )
+ end
+
+ subject { described_class.new(change_access) }
+
+ before do
+ project.add_developer(user)
+ end
+end
diff --git a/spec/support/shared_contexts/url_shared_context.rb b/spec/support/shared_contexts/url_shared_context.rb
new file mode 100644
index 00000000000..1b1f67daac3
--- /dev/null
+++ b/spec/support/shared_contexts/url_shared_context.rb
@@ -0,0 +1,17 @@
+shared_context 'invalid urls' do
+ let(:urls_with_CRLF) do
+ ["http://127.0.0.1:333/pa\rth",
+ "http://127.0.0.1:333/pa\nth",
+ "http://127.0a.0.1:333/pa\r\nth",
+ "http://127.0.0.1:333/path?param=foo\r\nbar",
+ "http://127.0.0.1:333/path?param=foo\rbar",
+ "http://127.0.0.1:333/path?param=foo\nbar",
+ "http://127.0.0.1:333/pa%0dth",
+ "http://127.0.0.1:333/pa%0ath",
+ "http://127.0a.0.1:333/pa%0d%0th",
+ "http://127.0.0.1:333/pa%0D%0Ath",
+ "http://127.0.0.1:333/path?param=foo%0Abar",
+ "http://127.0.0.1:333/path?param=foo%0Dbar",
+ "http://127.0.0.1:333/path?param=foo%0D%0Abar"]
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index 377bd82b67e..c603421d748 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -180,10 +180,9 @@ shared_examples_for 'common trace features' do
end
context 'runners token' do
- let(:token) { 'my_secret_token' }
+ let(:token) { build.project.runners_token }
before do
- build.project.update(runners_token: token)
trace.set(token)
end
@@ -193,10 +192,9 @@ shared_examples_for 'common trace features' do
end
context 'hides build token' do
- let(:token) { 'my_secret_token' }
+ let(:token) { build.token }
before do
- build.update(token: token)
trace.set(token)
end
diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb
index 55ce160add0..367ddf06c28 100644
--- a/spec/support/shared_examples/diff_file_collections.rb
+++ b/spec/support/shared_examples/diff_file_collections.rb
@@ -45,3 +45,19 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
end
end
end
+
+shared_examples 'unfoldable diff' do
+ let(:subject) { described_class.new(diffable, diff_options: nil) }
+
+ it 'calls Gitlab::Diff::File#unfold_diff_lines with correct position' do
+ position = instance_double(Gitlab::Diff::Position, file_path: 'README')
+ readme_file = instance_double(Gitlab::Diff::File, file_path: 'README')
+ other_file = instance_double(Gitlab::Diff::File, file_path: 'foo.rb')
+ nil_path_file = instance_double(Gitlab::Diff::File, file_path: nil)
+
+ allow(subject).to receive(:diff_files) { [readme_file, other_file, nil_path_file] }
+ expect(readme_file).to receive(:unfold_diff_lines).with(position)
+
+ subject.unfold_diff_files([position])
+ 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/only_except_policy_examples.rb b/spec/support/shared_examples/only_except_policy_examples.rb
new file mode 100644
index 00000000000..35240af1d74
--- /dev/null
+++ b/spec/support/shared_examples/only_except_policy_examples.rb
@@ -0,0 +1,167 @@
+# 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/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 4eda618b6d6..06525e3c771 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -1,51 +1,101 @@
require 'rake_helper'
-describe 'gitlab:ldap:check rake task' do
- include LdapHelpers
-
+describe 'check.rake' do
before do
Rake.application.rake_require 'tasks/gitlab/check'
stub_warn_user_is_not_gitlab
end
- context 'when LDAP is not enabled' do
- it 'does not attempt to bind or search for users' do
- expect(Gitlab::Auth::LDAP::Config).not_to receive(:providers)
- expect(Gitlab::Auth::LDAP::Adapter).not_to receive(:open)
-
- run_rake_task('gitlab:ldap:check')
+ shared_examples_for 'system check rake task' do
+ it 'runs the check' do
+ expect do
+ subject
+ end.to output(/Checking #{name} ... Finished/).to_stdout
end
end
- context 'when LDAP is enabled' do
- let(:ldap) { double(:ldap) }
- let(:adapter) { ldap_adapter('ldapmain', ldap) }
+ describe 'gitlab:check rake task' do
+ subject { run_rake_task('gitlab:check') }
+ let(:name) { 'GitLab subtasks' }
- before do
- allow(Gitlab::Auth::LDAP::Config)
- .to receive_messages(
- enabled?: true,
- providers: ['ldapmain']
- )
- allow(Gitlab::Auth::LDAP::Adapter).to receive(:open).and_yield(adapter)
- allow(adapter).to receive(:users).and_return([])
- end
+ it_behaves_like 'system check rake task'
+ end
+
+ describe 'gitlab:gitlab_shell:check rake task' do
+ subject { run_rake_task('gitlab:gitlab_shell:check') }
+ let(:name) { 'GitLab Shell' }
+
+ it_behaves_like 'system check rake task'
+ end
+
+ describe 'gitlab:gitaly:check rake task' do
+ subject { run_rake_task('gitlab:gitaly:check') }
+ let(:name) { 'Gitaly' }
+
+ it_behaves_like 'system check rake task'
+ end
+
+ describe 'gitlab:sidekiq:check rake task' do
+ subject { run_rake_task('gitlab:sidekiq:check') }
+ let(:name) { 'Sidekiq' }
- it 'attempts to bind using credentials' do
- stub_ldap_config(has_auth?: true)
+ it_behaves_like 'system check rake task'
+ end
- expect(ldap).to receive(:bind)
+ describe 'gitlab:incoming_email:check rake task' do
+ subject { run_rake_task('gitlab:incoming_email:check') }
+ let(:name) { 'Incoming Email' }
- run_rake_task('gitlab:ldap:check')
+ it_behaves_like 'system check rake task'
+ end
+
+ describe 'gitlab:ldap:check rake task' do
+ include LdapHelpers
+
+ subject { run_rake_task('gitlab:ldap:check') }
+ let(:name) { 'LDAP' }
+
+ it_behaves_like 'system check rake task'
+
+ context 'when LDAP is not enabled' do
+ it 'does not attempt to bind or search for users' do
+ expect(Gitlab::Auth::LDAP::Config).not_to receive(:providers)
+ expect(Gitlab::Auth::LDAP::Adapter).not_to receive(:open)
+
+ subject
+ end
end
- it 'searches for 100 LDAP users' do
- stub_ldap_config(uid: 'uid')
+ context 'when LDAP is enabled' do
+ let(:ldap) { double(:ldap) }
+ let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+ before do
+ allow(Gitlab::Auth::LDAP::Config)
+ .to receive_messages(
+ enabled?: true,
+ providers: ['ldapmain']
+ )
+ allow(Gitlab::Auth::LDAP::Adapter).to receive(:open).and_yield(adapter)
+ allow(adapter).to receive(:users).and_return([])
+ end
+
+ it 'attempts to bind using credentials' do
+ stub_ldap_config(has_auth?: true)
+
+ expect(ldap).to receive(:bind)
+
+ subject
+ end
+
+ it 'searches for 100 LDAP users' do
+ stub_ldap_config(uid: 'uid')
- expect(adapter).to receive(:users).with('uid', '*', 100)
+ expect(adapter).to receive(:users).with('uid', '*', 100)
- run_rake_task('gitlab:ldap:check')
+ subject
+ end
end
end
end
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/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
index ab6100509a6..082d09d3f16 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/url_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UrlValidator do
@@ -6,6 +8,30 @@ describe UrlValidator do
include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+ describe 'validations' do
+ include_context 'invalid urls'
+
+ let(:validator) { described_class.new(attributes: [:link_url]) }
+
+ it 'returns error when url is nil' do
+ expect(validator.validate_each(badge, :link_url, nil)).to be_nil
+ expect(badge.errors.first[1]).to eq 'must be a valid URL'
+ end
+
+ it 'returns error when url is empty' do
+ expect(validator.validate_each(badge, :link_url, '')).to be_nil
+ expect(badge.errors.first[1]).to eq 'must be a valid URL'
+ end
+
+ it 'does not allow urls with CR or LF characters' do
+ aggregate_failures do
+ urls_with_CRLF.each do |url|
+ expect(validator.validate_each(badge, :link_url, url)[0]).to eq 'is blocked: URI is invalid'
+ end
+ end
+ end
+ end
+
context 'by default' do
let(:validator) { described_class.new(attributes: [:link_url]) }
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index 1215908f5ea..05c2f61a606 100644
--- a/spec/features/admin/admin_active_tab_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -1,27 +1,26 @@
require 'spec_helper'
-RSpec.describe 'admin active tab' do
- before do
- sign_in(create(:admin))
- end
-
+describe 'layouts/nav/sidebar/_admin' do
shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
- expect(page).to have_selector('.nav-sidebar .sidebar-top-level-items > li.active', count: 1)
- expect(page.find('.nav-sidebar .sidebar-top-level-items > li.active')).to have_content(title)
+ render
+
+ expect(rendered).to have_selector('.nav-sidebar .sidebar-top-level-items > li.active', count: 1)
+ expect(rendered).to have_css('.nav-sidebar .sidebar-top-level-items > li.active', text: title)
end
end
shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 2)
- expect(page.all('.sidebar-sub-level-items > li.active')[1]).to have_content(title)
+ render
+
+ expect(rendered).to have_css('.sidebar-sub-level-items > li.active', text: title)
end
end
context 'on home page' do
before do
- visit admin_root_path
+ allow(controller).to receive(:controller_name).and_return('dashboard')
end
it_behaves_like 'page has active tab', 'Overview'
@@ -29,7 +28,8 @@ RSpec.describe 'admin active tab' do
context 'on projects' do
before do
- visit admin_projects_path
+ allow(controller).to receive(:controller_name).and_return('projects')
+ allow(controller).to receive(:controller_path).and_return('admin/projects')
end
it_behaves_like 'page has active tab', 'Overview'
@@ -38,7 +38,7 @@ RSpec.describe 'admin active tab' do
context 'on groups' do
before do
- visit admin_groups_path
+ allow(controller).to receive(:controller_name).and_return('groups')
end
it_behaves_like 'page has active tab', 'Overview'
@@ -47,7 +47,7 @@ RSpec.describe 'admin active tab' do
context 'on users' do
before do
- visit admin_users_path
+ allow(controller).to receive(:controller_name).and_return('users')
end
it_behaves_like 'page has active tab', 'Overview'
@@ -56,7 +56,7 @@ RSpec.describe 'admin active tab' do
context 'on logs' do
before do
- visit admin_logs_path
+ allow(controller).to receive(:controller_name).and_return('logs')
end
it_behaves_like 'page has active tab', 'Monitoring'
@@ -65,7 +65,7 @@ RSpec.describe 'admin active tab' do
context 'on messages' do
before do
- visit admin_broadcast_messages_path
+ allow(controller).to receive(:controller_name).and_return('broadcast_messages')
end
it_behaves_like 'page has active tab', 'Messages'
@@ -73,7 +73,7 @@ RSpec.describe 'admin active tab' do
context 'on hooks' do
before do
- visit admin_hooks_path
+ allow(controller).to receive(:controller_name).and_return('hooks')
end
it_behaves_like 'page has active tab', 'Hooks'
@@ -81,7 +81,7 @@ RSpec.describe 'admin active tab' do
context 'on background jobs' do
before do
- visit admin_background_jobs_path
+ allow(controller).to receive(:controller_name).and_return('background_jobs')
end
it_behaves_like 'page has active tab', 'Monitoring'
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/archive_trace_worker_spec.rb b/spec/workers/archive_trace_worker_spec.rb
index b768588c6e1..7244ad4f199 100644
--- a/spec/workers/archive_trace_worker_spec.rb
+++ b/spec/workers/archive_trace_worker_spec.rb
@@ -5,10 +5,11 @@ describe ArchiveTraceWorker do
subject { described_class.new.perform(job&.id) }
context 'when job is found' do
- let(:job) { create(:ci_build) }
+ let(:job) { create(:ci_build, :trace_live) }
it 'executes service' do
- expect_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!)
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .to receive(:execute).with(job)
subject
end
@@ -18,7 +19,8 @@ describe ArchiveTraceWorker do
let(:job) { nil }
it 'does not execute service' do
- expect_any_instance_of(Gitlab::Ci::Trace).not_to receive(:archive!)
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .not_to receive(:execute)
subject
end
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 23f5dda298a..478fb7d2c0f 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -30,6 +30,13 @@ describe Ci::ArchiveTracesCronWorker do
it_behaves_like 'archives trace'
+ it 'executes service' do
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .to receive(:execute).with(build)
+
+ subject
+ end
+
context 'when a trace had already been archived' do
let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) }
let!(:build2) { create(:ci_build, :success, :trace_live) }
@@ -46,11 +53,12 @@ describe Ci::ArchiveTracesCronWorker do
let!(:build) { create(:ci_build, :success, :trace_live) }
before do
+ allow(Gitlab::Sentry).to receive(:track_exception)
allow_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!).and_raise('Unexpected error')
end
it 'puts a log' do
- expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Unexpected error")
+ expect(Rails.logger).to receive(:error).with("Failed to archive trace. id: #{build.id} message: Unexpected error")
subject
end
diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb
index b51f6e07c6a..0eead0ab13d 100644
--- a/spec/workers/cluster_platform_configure_worker_spec.rb
+++ b/spec/workers/cluster_platform_configure_worker_spec.rb
@@ -2,7 +2,43 @@
require 'spec_helper'
-describe ClusterPlatformConfigureWorker, '#execute' do
+describe ClusterPlatformConfigureWorker, '#perform' do
+ let(:worker) { described_class.new }
+
+ context 'when group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+
+ context 'when group has no projects' do
+ it 'does not create a namespace' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+
+ worker.perform(cluster.id)
+ end
+ end
+
+ context 'when group has a project' do
+ let!(:project) { create(:project, group: group) }
+
+ it 'creates a namespace for the project' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+
+ worker.perform(cluster.id)
+ end
+ end
+
+ context 'when group has project in a sub-group' do
+ let!(:subgroup) { create(:group, parent: group) }
+ let!(:project) { create(:project, group: subgroup) }
+
+ it 'creates a namespace for the project' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+
+ worker.perform(cluster.id)
+ end
+ end
+ end
+
context 'when provider type is gcp' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
@@ -30,18 +66,4 @@ describe ClusterPlatformConfigureWorker, '#execute' do
described_class.new.perform(123)
end
end
-
- context 'when kubeclient raises error' do
- let(:cluster) { create(:cluster, :project) }
-
- it 'rescues and logs the error' do
- allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).and_raise(::Kubeclient::HttpError.new(500, 'something baaaad happened', ''))
-
- expect(Rails.logger)
- .to receive(:error)
- .with("Failed to create/update Kubernetes namespace for cluster_id: #{cluster.id} with error: something baaaad happened")
-
- described_class.new.perform(cluster.id)
- end
- end
end
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index 241e8a2b6d3..d85a87f2cb0 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -58,14 +58,16 @@ describe Gitlab::GithubImport::StageMethods do
end
describe '#find_project' do
+ let(:import_state) { create(:import_state, project: project) }
+
it 'returns a Project for an existing ID' do
- project.update_column(:import_status, 'started')
+ import_state.update_column(:status, 'started')
expect(worker.find_project(project.id)).to eq(project)
end
it 'returns nil for a project that failed importing' do
- project.update_column(:import_status, 'failed')
+ import_state.update_column(:status, 'failed')
expect(worker.find_project(project.id)).to be_nil
end
diff --git a/spec/workers/concerns/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb
index b6c111df8b9..3699fd83a9a 100644
--- a/spec/workers/concerns/project_import_options_spec.rb
+++ b/spec/workers/concerns/project_import_options_spec.rb
@@ -28,13 +28,23 @@ describe ProjectImportOptions do
worker_class.sidekiq_retries_exhausted_block.call(job)
- expect(project.reload.import_error).to include("fork")
+ expect(project.import_state.reload.last_error).to include("fork")
end
it 'logs the appropriate error message for forked projects' do
worker_class.sidekiq_retries_exhausted_block.call(job)
- expect(project.reload.import_error).to include("import")
+ expect(project.import_state.reload.last_error).to include("import")
+ end
+
+ context 'when project does not have import_state' do
+ let(:project) { create(:project) }
+
+ it 'raises an error' do
+ expect do
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end.to raise_error(NoMethodError)
+ end
end
end
end
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
index 0f78c5cc644..fc7aafbc0c9 100644
--- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -17,8 +17,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st
context 'when there are remaining jobs' do
before do
allow(worker)
- .to receive(:find_project)
- .and_return(project)
+ .to receive(:find_import_state)
+ .and_return(import_state)
end
it 'reschedules itself' do
@@ -38,8 +38,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st
context 'when there are no remaining jobs' do
before do
allow(worker)
- .to receive(:find_project)
- .and_return(project)
+ .to receive(:find_import_state)
+ .and_return(import_state)
allow(worker)
.to receive(:wait_for_jobs)
@@ -48,8 +48,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st
end
it 'schedules the next stage' do
- expect(project)
- .to receive(:refresh_import_jid_expiration)
+ expect(import_state)
+ .to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::Stage::FinishImportWorker)
.to receive(:perform_async)
@@ -96,22 +96,18 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st
end
end
- describe '#find_project' do
- it 'returns a Project' do
- project.update_column(:import_status, 'started')
+ describe '#find_import_state' do
+ it 'returns a ProjectImportState' do
+ import_state.update_column(:status, 'started')
- found = worker.find_project(project.id)
+ found = worker.find_import_state(project.id)
- expect(found).to be_an_instance_of(Project)
-
- # This test is there to make sure we only select the columns we care
- # about.
- # TODO: enable this assertion back again
- # expect(found.attributes).to include({ 'id' => nil, 'import_jid' => '123' })
+ expect(found).to be_an_instance_of(ProjectImportState)
+ expect(found.attributes.keys).to match_array(%w(id jid))
end
it 'returns nil if the project import is not running' do
- expect(worker.find_project(project.id)).to be_nil
+ expect(worker.find_import_state(project.id)).to be_nil
end
end
end
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
index 25ada575a44..7ff133f1049 100644
--- a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
context 'when the job is running' do
it 'refreshes the import JID and reschedules itself' do
allow(worker)
- .to receive(:find_project)
+ .to receive(:find_import_state)
.with(project.id)
.and_return(project)
@@ -39,7 +39,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
.and_return(true)
expect(project)
- .to receive(:refresh_import_jid_expiration)
+ .to receive(:refresh_jid_expiration)
expect(worker.class)
.to receive(:perform_in_the_future)
@@ -52,7 +52,7 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
context 'when the job is no longer running' do
it 'returns' do
allow(worker)
- .to receive(:find_project)
+ .to receive(:find_import_state)
.with(project.id)
.and_return(project)
@@ -62,18 +62,18 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
.and_return(false)
expect(project)
- .not_to receive(:refresh_import_jid_expiration)
+ .not_to receive(:refresh_jid_expiration)
worker.perform(project.id, '123')
end
end
end
- describe '#find_project' do
- it 'returns a Project' do
+ describe '#find_import_state' do
+ it 'returns a ProjectImportState' do
project = create(:project, :import_started)
- expect(worker.find_project(project.id)).to be_an_instance_of(Project)
+ expect(worker.find_import_state(project.id)).to be_an_instance_of(ProjectImportState)
end
# it 'only selects the import JID field' do
@@ -84,14 +84,14 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
# .to eq({ 'id' => nil, 'import_jid' => '123abc' })
# end
- it 'returns nil for a project for which the import process failed' do
+ it 'returns nil for a import state for which the import process failed' do
project = create(:project, :import_failed)
- expect(worker.find_project(project.id)).to be_nil
+ expect(worker.find_import_state(project.id)).to be_nil
end
- it 'returns nil for a non-existing project' do
- expect(worker.find_project(-1)).to be_nil
+ it 'returns nil for a non-existing find_import_state' do
+ expect(worker.find_import_state(-1)).to be_nil
end
end
end
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
index 8c80d660287..ad6154cc4a4 100644
--- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
let(:project) { create(:project) }
+ let(:import_state) { create(:import_state, project: project) }
let(:worker) { described_class.new }
describe '#import' do
@@ -18,7 +19,7 @@ describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
expect(importer).to receive(:execute)
end
- expect(project).to receive(:refresh_import_jid_expiration)
+ expect(import_state).to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::Stage::ImportPullRequestsWorker)
.to receive(:perform_async)
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
index 2fc91a3e80a..1fbb073a34a 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
let(:project) { create(:project) }
+ let(:import_state) { create(:import_state, project: project) }
let(:worker) { described_class.new }
describe '#import' do
@@ -19,8 +20,8 @@ describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
.to receive(:execute)
.and_return(waiter)
- expect(project)
- .to receive(:refresh_import_jid_expiration)
+ expect(import_state)
+ .to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index a2fe4734d47..ff408427926 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -11,6 +11,7 @@ describe PipelineScheduleWorker do
end
before do
+ stub_application_setting(auto_devops_enabled: false)
stub_ci_pipeline_to_return_yaml_file
pipeline_schedule.update_column(:next_run_at, 1.day.ago)
@@ -24,12 +25,12 @@ describe PipelineScheduleWorker do
context 'when there is a scheduled pipeline within next_run_at' do
shared_examples 'successful scheduling' do
it 'creates a new pipeline' do
- expect { subject }.to change { project.pipelines.count }.by(1)
+ expect { subject }.to change { project.ci_pipelines.count }.by(1)
expect(Ci::Pipeline.last).to be_schedule
pipeline_schedule.reload
expect(pipeline_schedule.next_run_at).to be > Time.now
- expect(pipeline_schedule).to eq(project.pipelines.last.pipeline_schedule)
+ expect(pipeline_schedule).to eq(project.ci_pipelines.last.pipeline_schedule)
expect(pipeline_schedule).to be_active
end
end
@@ -53,20 +54,92 @@ describe PipelineScheduleWorker do
end
it 'does not creates a new pipeline' do
- expect { subject }.not_to change { project.pipelines.count }
+ expect { subject }.not_to change { project.ci_pipelines.count }
+ end
+ end
+
+ context 'when gitlab-ci.yml is corrupted' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
+ end
+
+ it 'creates a failed pipeline with the reason' do
+ expect { subject }.to change { project.ci_pipelines.count }.by(1)
+ expect(Ci::Pipeline.last).to be_config_error
+ expect(Ci::Pipeline.last.yaml_errors).not_to be_nil
end
end
end
context 'when the schedule is not runnable by the user' do
- it 'deactivates the schedule' do
+ before do
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(Ci::CreatePipelineService::CreateError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'does not deactivate the schedule' do
subject
- expect(pipeline_schedule.reload.active).to be_falsy
+ expect(pipeline_schedule.reload.active).to be_truthy
+ end
+
+ it 'increments Prometheus counter' do
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
+ .and_call_original
+
+ subject
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching("Insufficient permissions to create a new pipeline"))
+ .and_call_original
+
+ subject
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
+ end
+
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when .gitlab-ci.yml is missing in the project' do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ project.add_maintainer(user)
+
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(Ci::CreatePipelineService::CreateError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching("Missing .gitlab-ci.yml file"))
+ .and_call_original
+
+ subject
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
end
- it 'does not schedule a pipeline' do
- expect { subject }.not_to change { project.pipelines.count }
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
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_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index d07e40377d4..87ac4bc05c1 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -9,13 +9,13 @@ describe RepositoryImportWorker do
describe '#perform' do
let(:project) { create(:project, :import_scheduled) }
+ let(:import_state) { project.import_state }
context 'when worker was reset without cleanup' do
it 'imports the project successfully' do
jid = '12345678'
started_project = create(:project)
-
- create(:import_state, :started, project: started_project, jid: jid)
+ started_import_state = create(:import_state, :started, project: started_project, jid: jid)
allow(subject).to receive(:jid).and_return(jid)
@@ -23,12 +23,12 @@ describe RepositoryImportWorker do
.and_return({ status: :ok })
# Works around https://github.com/rspec/rspec-mocks/issues/910
- expect(Project).to receive(:find).with(project.id).and_return(project)
- expect(project.repository).to receive(:expire_emptiness_caches)
- expect(project.wiki.repository).to receive(:expire_emptiness_caches)
- expect(project).to receive(:import_finish)
+ expect(Project).to receive(:find).with(started_project.id).and_return(started_project)
+ expect(started_project.repository).to receive(:expire_emptiness_caches)
+ expect(started_project.wiki.repository).to receive(:expire_emptiness_caches)
+ expect(started_import_state).to receive(:finish)
- subject.perform(project.id)
+ subject.perform(started_project.id)
end
end
@@ -41,7 +41,7 @@ describe RepositoryImportWorker do
expect(Project).to receive(:find).with(project.id).and_return(project)
expect(project.repository).to receive(:expire_emptiness_caches)
expect(project.wiki.repository).to receive(:expire_emptiness_caches)
- expect(project).to receive(:import_finish)
+ expect(import_state).to receive(:finish)
subject.perform(project.id)
end
@@ -51,26 +51,27 @@ describe RepositoryImportWorker do
it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update(import_jid: '123')
+ import_state.update(jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
subject.perform(project.id)
end.to raise_error(RuntimeError, error)
- expect(project.reload.import_jid).not_to be_nil
+ expect(import_state.reload.jid).not_to be_nil
end
it 'updates the error on Import/Export' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update(import_jid: '123', import_type: 'gitlab_project')
+ project.update(import_type: 'gitlab_project')
+ import_state.update(jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
subject.perform(project.id)
end.to raise_error(RuntimeError, error)
- expect(project.reload.import_error).not_to be_nil
+ expect(import_state.reload.last_error).not_to be_nil
end
end
@@ -90,8 +91,8 @@ describe RepositoryImportWorker do
.to receive(:async?)
.and_return(true)
- expect_any_instance_of(Project)
- .not_to receive(:import_finish)
+ expect_any_instance_of(ProjectImportState)
+ .not_to receive(:finish)
subject.perform(project.id)
end
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index 9adde5fc21a..a2bc264b0f6 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -34,5 +34,33 @@ describe UpdateHeadPipelineForMergeRequestWorker do
expect { subject.perform(merge_request.id) }.not_to change { merge_request.reload.head_pipeline_id }
end
end
+
+ context 'when a merge request pipeline exists' do
+ let!(:merge_request_pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ source: :merge_request,
+ sha: latest_sha,
+ merge_request: merge_request)
+ end
+
+ it 'sets the merge request pipeline as the head pipeline' do
+ expect { subject.perform(merge_request.id) }
+ .to change { merge_request.reload.head_pipeline_id }
+ .from(nil).to(merge_request_pipeline.id)
+ end
+
+ context 'when branch pipeline exists' do
+ let!(:branch_pipeline) do
+ create(:ci_pipeline, project: project, source: :push, sha: latest_sha)
+ end
+
+ it 'prioritizes the merge request pipeline as the head pipeline' do
+ expect { subject.perform(merge_request.id) }
+ .to change { merge_request.reload.head_pipeline_id }
+ .from(nil).to(merge_request_pipeline.id)
+ end
+ end
+ end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 24136a7aca5..781d6e3042f 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -22,3 +22,4 @@ ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
+ kubernetes.io/tls-acme: "true"
diff --git a/yarn.lock b/yarn.lock
index f47045ad6f6..d7d2b89a881 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,7 +2,7 @@
# yarn lockfile v1
-"@babel/code-frame@^7.0.0":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
@@ -629,19 +629,20 @@
eslint-plugin-promise "^4.0.1"
eslint-plugin-vue "^5.0.0-beta.3"
-"@gitlab/svgs@^1.38.0":
- version "1.38.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.38.0.tgz#e2f6e73379d60c7c63af4df8242a94c4671a1dfe"
- integrity sha512-Mzv6PxVbWEPvvMgXHaGxk8UE1Gard2gifca6loLgfLH7BtjXfESiZyJdQkkTSeBYp5MoqQa88Kw+vJYobwjsSw==
+"@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/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.14.0":
+ version "1.14.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.14.0.tgz#f0fd7c0e6c45a36ab3be18d00e2908a8cb405f90"
+ integrity sha512-jkBTN8qO41A894kcLo6b/mfLIgL8YNn+ZzjgzEXaZ3PyeQ3mKBdrBoSYkzH556qviroBvk/+3yyZz96VUo08qQ==
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"
@@ -882,6 +883,11 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8"
integrity sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==
+abab@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+ integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==
+
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@@ -902,6 +908,14 @@ acorn-dynamic-import@^3.0.0:
dependencies:
acorn "^5.0.0"
+acorn-globals@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103"
+ integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==
+ dependencies:
+ acorn "^6.0.1"
+ acorn-walk "^6.0.1"
+
acorn-jsx@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e"
@@ -909,11 +923,21 @@ acorn-jsx@^4.1.1:
dependencies:
acorn "^5.0.3"
-acorn@^5.0.0, acorn@^5.0.3, acorn@^5.6.0, acorn@^5.6.2, acorn@^5.7.3:
+acorn-walk@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
+ integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
+
+acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2, acorn@^5.7.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+acorn@^6.0.1:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754"
+ integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==
+
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -939,6 +963,16 @@ ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+ajv@^6.5.5:
+ version "6.5.5"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
+ integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
+ 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"
@@ -986,7 +1020,7 @@ ansi-styles@^2.2.1:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
-ansi-styles@^3.2.1:
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
@@ -1098,6 +1132,13 @@ apollo-utilities@1.0.25, apollo-utilities@^1.0.0, apollo-utilities@^1.0.25, apol
dependencies:
fast-json-stable-stringify "^2.0.0"
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ integrity sha1-126/jKlNJ24keja61EpLdKthGZE=
+ dependencies:
+ default-require-extensions "^1.0.0"
+
append-transform@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab"
@@ -1125,12 +1166,19 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=
+ dependencies:
+ arr-flatten "^1.0.1"
+
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
-arr-flatten@^1.1.0:
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
@@ -1140,6 +1188,11 @@ arr-union@^3.1.0:
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+ integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
array-find@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8"
@@ -1201,6 +1254,18 @@ asn1.js@^4.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
@@ -1213,6 +1278,11 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+ integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
+
async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -1228,13 +1298,18 @@ async@1.x, async@^1.5.2:
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
-async@^2.0.0, async@^2.5.0, async@^2.6.1:
+async@^2.0.0, async@^2.1.4, async@^2.5.0, async@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
dependencies:
lodash "^4.17.10"
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
atob@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@@ -1245,6 +1320,16 @@ autosize@^4.0.0:
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237"
integrity sha1-egWZsbqE1zvXWJsNnaOHAVLGkjc=
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
axios-mock-adapter@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz#fbc06825d8302c95c3334d21023bba996255d45d"
@@ -1269,6 +1354,36 @@ babel-code-frame@^6.26.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
+babel-core@^6.0.0, babel-core@^6.26.0:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
+ integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.8"
+ slash "^1.0.0"
+ source-map "^0.5.7"
+
+babel-core@^7.0.0-bridge:
+ version "7.0.0-bridge.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
+ integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
+
babel-eslint@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
@@ -1281,6 +1396,36 @@ babel-eslint@^10.0.1:
eslint-scope "3.7.1"
eslint-visitor-keys "^1.0.0"
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-jest@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1"
+ integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==
+ dependencies:
+ babel-plugin-istanbul "^4.1.6"
+ babel-preset-jest "^23.2.0"
+
babel-loader@^8.0.4:
version "8.0.4"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.4.tgz#7bbf20cbe4560629e2e41534147692d3fecbdce6"
@@ -1298,6 +1443,23 @@ babel-messages@^6.23.0:
dependencies:
babel-runtime "^6.22.0"
+babel-plugin-dynamic-import-node@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz#c0adfb07d95f4a4495e9aaac6ec386c4d7c2524e"
+ integrity sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA==
+ dependencies:
+ object.assign "^4.1.0"
+
+babel-plugin-istanbul@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
+ integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
+
babel-plugin-istanbul@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.0.tgz#6892f529eff65a3e2d33d87dc5888ffa2ecd4a30"
@@ -1307,11 +1469,39 @@ babel-plugin-istanbul@^5.1.0:
istanbul-lib-instrument "^3.0.0"
test-exclude "^5.0.0"
+babel-plugin-jest-hoist@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167"
+ integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc=
+
babel-plugin-rewire@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz#822562d72ed2c84e47c0f95ee232c920853e9d89"
integrity sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==
+babel-plugin-syntax-object-rest-spread@^6.13.0:
+ version "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:
+ 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==
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
babel-polyfill@6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d"
@@ -1321,6 +1511,27 @@ babel-polyfill@6.23.0:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"
+babel-preset-jest@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46"
+ integrity sha1-jsegOhOPABoaj7HoETZSvxpV2kY=
+ dependencies:
+ babel-plugin-jest-hoist "^23.2.0"
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ integrity sha1-btAhFz4vy0htestFxgCahW9kcHE=
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
@@ -1334,7 +1545,7 @@ babel-standalone@^6.26.0:
resolved "https://registry.yarnpkg.com/babel-standalone/-/babel-standalone-6.26.0.tgz#15fb3d35f2c456695815ebf1ed96fe7f015b6886"
integrity sha1-Ffs9NfLEVmlYFevx7Zb+fwFbaIY=
-babel-template@^6.26.0:
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=
@@ -1345,7 +1556,7 @@ babel-template@^6.26.0:
babylon "^6.18.0"
lodash "^4.17.4"
-babel-traverse@^6.26.0:
+babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
@@ -1360,7 +1571,7 @@ babel-traverse@^6.26.0:
invariant "^2.2.2"
lodash "^4.17.4"
-babel-types@^6.26.0:
+babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
@@ -1418,6 +1629,13 @@ batch@0.6.1:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
better-assert@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
@@ -1505,10 +1723,10 @@ bootstrap-vue@^2.0.0-rc.11:
popper.js "^1.12.9"
vue-functional-data-merge "^2.0.5"
-bootstrap@4.1.1, bootstrap@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb"
- integrity sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg==
+bootstrap@4.1.3, bootstrap@^4.1.1:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
+ integrity sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w==
boxen@^1.2.1:
version "1.3.0"
@@ -1538,6 +1756,15 @@ braces@^0.1.2:
dependencies:
expand-range "^0.1.0"
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
braces@^2.3.0, braces@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.1.tgz#7086c913b4e5a08dbe37ac0ee6a2500c4ba691bb"
@@ -1561,6 +1788,18 @@ brorand@^1.0.1:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+browser-process-hrtime@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
+ integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==
+
+browser-resolve@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+ integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
+ dependencies:
+ resolve "1.1.7"
+
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
@@ -1628,6 +1867,13 @@ browserslist@^4.1.0:
electron-to-chromium "^1.3.62"
node-releases "^1.0.0-alpha.11"
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=
+ dependencies:
+ node-int64 "^0.4.0"
+
buffer-from@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
@@ -1761,6 +2007,11 @@ callsites@^0.2.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+ integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
camelcase@^4.0.0, camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
@@ -1771,11 +2022,23 @@ caniuse-lite@^1.0.30000884:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000888.tgz#22edb50d91dd70612b5898e3b36f460600c6492f"
integrity sha512-vftg+5p/lPsQGpnhSo/yBuYL36ai/cyjLvU3dOPJY1kkKrekLWIy8SLm+wzjX0hpCUdFTasC4/ZT7uqw4rKOnQ==
+capture-exit@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
+ integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=
+ dependencies:
+ rsvp "^3.3.3"
+
capture-stack-trace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d"
integrity sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -1853,6 +2116,11 @@ chrome-trace-event@^1.0.0:
dependencies:
tslib "^1.9.0"
+ci-info@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+ integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@@ -1928,15 +2196,20 @@ clone-response@1.0.2:
dependencies:
mimic-response "^1.0.0"
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-codesandbox-api@^0.0.18:
- version "0.0.18"
- resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.18.tgz#56b96b37533f80d20c21861e5e477d3557e613ca"
- integrity sha512-DHLR8QQpMplNDF9GDbV8EevwmF9mlMBQwiWB8bZBnP2NQQbklthqjpBwNjah8qlDgfD7vQNNcwT8uIZ24WZb7Q==
+codesandbox-api@^0.0.20:
+ version "0.0.20"
+ resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.20.tgz#174bcd76c9f31521175c6bceabc37da6b1fbc30b"
+ integrity sha512-jhxZzAmjCKBZad8QWMeueiQVFE87igK6F2DBOEVFFJO6jgTXT8qjuzGYepr+B8bjgo/icN7bc/2xmEMBA63s2w==
codesandbox-import-util-types@^1.2.11:
version "1.2.11"
@@ -1984,6 +2257,13 @@ combine-lists@^1.0.0:
dependencies:
lodash "^4.5.0"
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
commander@2, commander@^2.18.0, commander@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
@@ -2145,7 +2425,7 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-convert-source-map@^1.1.0:
+convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
@@ -2186,7 +2466,7 @@ copy-to-clipboard@^3.0.8:
dependencies:
toggle-selection "^1.0.3"
-core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==
@@ -2196,7 +2476,7 @@ core-js@~2.3.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
integrity sha1-+rg/uwstjchfpjbEudNMdUIMbWU=
-core-util-is@~1.0.0:
+core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
@@ -2329,6 +2609,18 @@ cssesc@^0.1.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
+ integrity sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==
+
+cssstyle@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb"
+ integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==
+ dependencies:
+ cssom "0.3.x"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -2415,12 +2707,7 @@ d3-force@1.1.0:
d3-quadtree "1"
d3-timer "1"
-d3-format@1, d3-format@1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f"
- integrity sha512-U4zRVLDXW61bmqoo+OJ/V687e1T5nVd3TAKAJKgtpZ/P1JsMgyod0y9br+mlQOryTAACdiXI3wCjuERHFNp91w==
-
-d3-format@1.2.2:
+d3-format@1, d3-format@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw==
@@ -2492,12 +2779,7 @@ d3-scale@1.0.7, d3-scale@^1.0.7:
d3-time "1"
d3-time-format "2"
-d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0, d3-selection@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88"
- integrity sha512-xW2Pfcdzh1gOaoI+LGpPsLR2VpBQxuFoxvrvguK8ZmrJbPIVvfNG6pU6GNfK41D6Qz15sj61sbW/AFYuukwaLQ==
-
-d3-selection@1.3.0:
+d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA==
@@ -2554,42 +2836,6 @@ d3-zoom@1.7.1:
d3-selection "1"
d3-transition "1"
-d3@4.12.2:
- version "4.12.2"
- resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.2.tgz#12f775564c6a9de229f63db03446e2cb7bb56c8f"
- integrity sha512-aKAlpgTmpuGeEpezB+GvPpX1x+gCMs/PHpuse6sCpkgw4Un3ZeqUobIc87eIy9adcl+wxPAnEyKyO5oulH3MOw==
- dependencies:
- d3-array "1.2.1"
- d3-axis "1.0.8"
- d3-brush "1.0.4"
- d3-chord "1.0.4"
- d3-collection "1.0.4"
- d3-color "1.0.3"
- d3-dispatch "1.0.3"
- d3-drag "1.2.1"
- d3-dsv "1.0.8"
- d3-ease "1.0.3"
- d3-force "1.1.0"
- d3-format "1.2.1"
- d3-geo "1.9.1"
- d3-hierarchy "1.1.5"
- d3-interpolate "1.1.6"
- d3-path "1.0.5"
- d3-polygon "1.0.3"
- d3-quadtree "1.0.3"
- d3-queue "3.0.7"
- d3-random "1.1.0"
- d3-request "1.0.6"
- d3-scale "1.0.7"
- d3-selection "1.2.0"
- d3-shape "1.2.0"
- d3-time "1.0.8"
- d3-time-format "2.1.1"
- d3-timer "1.0.7"
- d3-transition "1.1.1"
- d3-voronoi "1.1.2"
- d3-zoom "1.7.1"
-
d3@^4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d"
@@ -2642,6 +2888,22 @@ dagre-layout@^0.8.8:
graphlibrary "^2.2.0"
lodash "^4.17.5"
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+data-urls@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+ integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+ dependencies:
+ abab "^2.0.0"
+ whatwg-mimetype "^2.2.0"
+ whatwg-url "^7.0.0"
+
date-format@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
@@ -2683,6 +2945,11 @@ debug@~3.1.0:
dependencies:
ms "2.0.0"
+decamelize@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
decamelize@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7"
@@ -2730,6 +2997,13 @@ default-gateway@^2.6.0:
execa "^0.10.0"
ip-regex "^2.1.0"
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=
+ dependencies:
+ strip-bom "^2.0.0"
+
default-require-extensions@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
@@ -2791,6 +3065,11 @@ del@^3.0.0:
pify "^3.0.0"
rimraf "^2.2.8"
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
delegate@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
@@ -2824,11 +3103,23 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg=
+ dependencies:
+ repeating "^2.0.0"
+
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+ integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
@@ -2926,6 +3217,13 @@ domelementtype@~1.1.1:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
domhandler@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
@@ -2973,6 +3271,21 @@ duplexify@^3.4.2, duplexify@^3.5.3:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ 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"
@@ -3181,6 +3494,18 @@ escodegen@1.8.x:
optionalDependencies:
source-map "~0.2.0"
+escodegen@^1.9.1:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589"
+ integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.6.1"
+
eslint-config-airbnb-base@^13.1.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz#b5a1b480b80dfad16433d6c4ad84e6605052c05c"
@@ -3197,6 +3522,15 @@ eslint-config-prettier@^3.1.0:
dependencies:
get-stdin "^6.0.0"
+eslint-import-resolver-jest@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-jest/-/eslint-import-resolver-jest-2.1.1.tgz#78c1934e3b5b77283326f036e089cc3b9fae6346"
+ integrity sha512-yzzZFN37CaMaCjmUZ4Zo7Pw5qCG/hDklVzxIeHYJZkbcdg0sL5MeLaOG8s3ndVBvv1PSdSq4jfkY0QXt/KPbTg==
+ dependencies:
+ find-root "^1.0.0"
+ micromatch "^3.1.6"
+ resolve "^1.5.0"
+
eslint-import-resolver-node@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
@@ -3267,6 +3601,11 @@ eslint-plugin-jasmine@^2.10.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz#5733b709e751f4bc40e31e1c16989bd2cdfbec97"
integrity sha1-VzO3CedR9LxA4x4cFpib0s377Jc=
+eslint-plugin-jest@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.1.0.tgz#9a4dfa3367563e8301560a7fb92ec309096dbca3"
+ integrity sha512-WcQd5LxEoAS20zuWEAd8CX0pVC+gGInZPcsoYvK5w7BrEJNmltyTxYYh1r0ct4GsahD2GvNySlcTcLtK2amFZA==
+
eslint-plugin-promise@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2"
@@ -3367,6 +3706,11 @@ esprima@2.7.x, esprima@^2.7.1:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+ integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
+
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@@ -3391,7 +3735,7 @@ estraverse@^1.9.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
integrity sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=
-estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
@@ -3449,6 +3793,13 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
+exec-sh@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
+ integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==
+ dependencies:
+ merge "^1.2.0"
+
execa@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
@@ -3475,6 +3826,11 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+ integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
expand-braces@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
@@ -3484,6 +3840,13 @@ expand-braces@^0.1.1:
array-unique "^0.2.1"
braces "^0.1.2"
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@@ -3505,6 +3868,25 @@ expand-range@^0.1.0:
is-number "^0.1.1"
repeat-string "^0.2.2"
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=
+ dependencies:
+ fill-range "^2.1.0"
+
+expect@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98"
+ integrity sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==
+ dependencies:
+ ansi-styles "^3.2.0"
+ jest-diff "^23.6.0"
+ jest-get-type "^22.1.0"
+ jest-matcher-utils "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-regex-util "^23.3.0"
+
exports-loader@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.7.0.tgz#84881c784dea6036b8e1cd1dac3da9b6409e21a5"
@@ -3564,7 +3946,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
assign-symbols "^1.0.0"
is-extendable "^1.0.1"
-extend@^3.0.0:
+extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
@@ -3587,6 +3969,13 @@ external-editor@^3.0.0:
iconv-lite "^0.4.22"
tmp "^0.0.33"
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=
+ dependencies:
+ is-extglob "^1.0.0"
+
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@@ -3601,6 +3990,16 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@@ -3635,6 +4034,13 @@ faye-websocket@~0.11.1:
dependencies:
websocket-driver ">=0.5.1"
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=
+ dependencies:
+ bser "^2.0.0"
+
figgy-pudding@^3.1.0, figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@@ -3663,7 +4069,12 @@ file-loader@^2.0.0:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
-fileset@^2.0.3:
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+ integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=
+
+fileset@^2.0.2, fileset@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=
@@ -3676,6 +4087,17 @@ filesize@^3.6.1:
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@@ -3730,7 +4152,7 @@ find-cache-dir@^2.0.0:
make-dir "^1.0.0"
pkg-dir "^3.0.0"
-find-root@^1.1.0:
+find-root@^1.0.0, find-root@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
@@ -3782,11 +4204,32 @@ follow-redirects@^1.2.5:
dependencies:
debug "^3.1.0"
-for-in@^1.0.2:
+for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
formdata-polyfill@^3.0.11:
version "3.0.11"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-3.0.11.tgz#c82b4b4bea3356c0a6752219e54ce1edb2a7fb5b"
@@ -3851,7 +4294,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-fsevents@^1.2.2:
+fsevents@^1.2.2, fsevents@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==
@@ -3908,6 +4351,13 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
gettext-extractor-vue@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.1.tgz#69d2737eb8f1938803ffcf9317133ed59fb2372f"
@@ -3930,6 +4380,21 @@ gettext-extractor@^3.3.2:
pofile "^1"
typescript "^2"
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=
+ dependencies:
+ is-glob "^2.0.0"
+
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -4084,6 +4549,11 @@ graphql@^14.0.2:
dependencies:
iterall "^1.2.2"
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+ integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
gzip-size@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80"
@@ -4097,7 +4567,7 @@ handle-thing@^1.2.5:
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=
-handlebars@^4.0.1, handlebars@^4.0.11:
+handlebars@^4.0.1, handlebars@^4.0.11, handlebars@^4.0.3:
version "4.0.12"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5"
integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==
@@ -4108,6 +4578,19 @@ handlebars@^4.0.1, handlebars@^4.0.11:
optionalDependencies:
uglify-js "^3.1.4"
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4244,6 +4727,14 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg=
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
hoopy@^0.1.2:
version "0.1.4"
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@@ -4264,6 +4755,13 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
html-entities@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
@@ -4319,6 +4817,15 @@ http-proxy@^1.13.0, http-proxy@^1.16.2:
eventemitter3 "1.x.x"
requires-port "1.x.x"
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@@ -4336,6 +4843,13 @@ iconv-lite@0.4.19:
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -4511,13 +5025,18 @@ into-stream@^3.1.0:
from2 "^2.1.1"
p-is-promise "^1.1.0"
-invariant@^2.2.2:
+invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
+
invert-kv@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
@@ -4581,6 +5100,13 @@ is-callable@^1.1.1, is-callable@^1.1.3:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+is-ci@^1.0.10:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+ integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+ dependencies:
+ ci-info "^1.5.0"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -4618,6 +5144,18 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
is-data-descriptor "^1.0.0"
kind-of "^6.0.2"
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+ integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=
+ dependencies:
+ is-primitive "^2.0.0"
+
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@@ -4630,11 +5168,23 @@ is-extendable@^1.0.1:
dependencies:
is-plain-object "^2.0.4"
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+ integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=
+
is-extglob@^2.1.0, is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
+ dependencies:
+ number-is-nan "^1.0.0"
+
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
@@ -4647,6 +5197,18 @@ is-fullwidth-code-point@^2.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+is-generator-fn@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a"
+ integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go=
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=
+ dependencies:
+ is-extglob "^1.0.0"
+
is-glob@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
@@ -4679,6 +5241,13 @@ is-number@^0.1.1:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=
+ dependencies:
+ kind-of "^3.0.2"
+
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -4739,6 +5308,16 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+ integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+ integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
+
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
@@ -4783,6 +5362,16 @@ is-symbol@^1.0.1:
dependencies:
has-symbols "^1.0.0"
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+ integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@@ -4825,6 +5414,28 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-api@^1.3.1:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa"
+ integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==
+ dependencies:
+ async "^2.1.4"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.2.1"
+ istanbul-lib-hook "^1.2.2"
+ istanbul-lib-instrument "^1.10.2"
+ istanbul-lib-report "^1.1.5"
+ istanbul-lib-source-maps "^1.2.6"
+ istanbul-reports "^1.5.1"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
istanbul-api@^2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-2.0.6.tgz#cd7b33ee678f6c01531d05f5e567ebbcd25f8ecc"
@@ -4843,11 +5454,23 @@ istanbul-api@^2.0.5:
make-dir "^1.3.0"
once "^1.4.0"
+istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0"
+ integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==
+
istanbul-lib-coverage@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#2aee0e073ad8c5f6a0b00e0dfbf52b4667472eda"
integrity sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==
+istanbul-lib-hook@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86"
+ integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==
+ dependencies:
+ append-transform "^0.4.0"
+
istanbul-lib-hook@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.1.tgz#918a57b75a0f951d552a08487ca1fa5336433d72"
@@ -4855,6 +5478,19 @@ istanbul-lib-hook@^2.0.1:
dependencies:
append-transform "^1.0.0"
+istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca"
+ integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.1"
+ semver "^5.3.0"
+
istanbul-lib-instrument@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz#b5f066b2a161f75788be17a9d556f40a0cf2afc9"
@@ -4868,6 +5504,16 @@ istanbul-lib-instrument@^3.0.0:
istanbul-lib-coverage "^2.0.1"
semver "^5.5.0"
+istanbul-lib-report@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c"
+ integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==
+ dependencies:
+ istanbul-lib-coverage "^1.2.1"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ supports-color "^3.1.2"
+
istanbul-lib-report@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.2.tgz#430a2598519113e1da7af274ba861bd42dd97535"
@@ -4877,6 +5523,17 @@ istanbul-lib-report@^2.0.2:
make-dir "^1.3.0"
supports-color "^5.4.0"
+istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f"
+ integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==
+ dependencies:
+ debug "^3.1.0"
+ istanbul-lib-coverage "^1.2.1"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
istanbul-lib-source-maps@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-2.0.1.tgz#ce8b45131d8293fdeaa732f4faf1852d13d0a97e"
@@ -4888,6 +5545,13 @@ istanbul-lib-source-maps@^2.0.1:
rimraf "^2.6.2"
source-map "^0.6.1"
+istanbul-reports@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a"
+ integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==
+ dependencies:
+ handlebars "^4.0.3"
+
istanbul-reports@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.0.1.tgz#fb8d6ea850701a3984350b977a969e9a556116a7"
@@ -4959,6 +5623,334 @@ jed@^1.1.1:
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
integrity sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=
+jest-changed-files@^23.4.2:
+ version "23.4.2"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83"
+ integrity sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==
+ dependencies:
+ throat "^4.0.0"
+
+jest-cli@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4"
+ integrity sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ import-local "^1.0.0"
+ is-ci "^1.0.10"
+ istanbul-api "^1.3.1"
+ istanbul-lib-coverage "^1.2.0"
+ istanbul-lib-instrument "^1.10.1"
+ istanbul-lib-source-maps "^1.2.4"
+ jest-changed-files "^23.4.2"
+ jest-config "^23.6.0"
+ jest-environment-jsdom "^23.4.0"
+ jest-get-type "^22.1.0"
+ jest-haste-map "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-regex-util "^23.3.0"
+ jest-resolve-dependencies "^23.6.0"
+ jest-runner "^23.6.0"
+ jest-runtime "^23.6.0"
+ jest-snapshot "^23.6.0"
+ jest-util "^23.4.0"
+ jest-validate "^23.6.0"
+ jest-watcher "^23.4.0"
+ jest-worker "^23.2.0"
+ micromatch "^2.3.11"
+ node-notifier "^5.2.1"
+ prompts "^0.1.9"
+ realpath-native "^1.0.0"
+ rimraf "^2.5.4"
+ slash "^1.0.0"
+ string-length "^2.0.0"
+ strip-ansi "^4.0.0"
+ which "^1.2.12"
+ yargs "^11.0.0"
+
+jest-config@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d"
+ integrity sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==
+ dependencies:
+ babel-core "^6.0.0"
+ babel-jest "^23.6.0"
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^23.4.0"
+ jest-environment-node "^23.4.0"
+ jest-get-type "^22.1.0"
+ jest-jasmine2 "^23.6.0"
+ jest-regex-util "^23.3.0"
+ jest-resolve "^23.6.0"
+ jest-util "^23.4.0"
+ jest-validate "^23.6.0"
+ micromatch "^2.3.11"
+ pretty-format "^23.6.0"
+
+jest-diff@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d"
+ integrity sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==
+ dependencies:
+ chalk "^2.0.1"
+ diff "^3.2.0"
+ jest-get-type "^22.1.0"
+ pretty-format "^23.6.0"
+
+jest-docblock@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-23.2.0.tgz#f085e1f18548d99fdd69b20207e6fd55d91383a7"
+ integrity sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-each@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575"
+ integrity sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==
+ dependencies:
+ chalk "^2.0.1"
+ pretty-format "^23.6.0"
+
+jest-environment-jsdom@^23.4.0:
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz#056a7952b3fea513ac62a140a2c368c79d9e6023"
+ integrity sha1-BWp5UrP+pROsYqFAosNox52eYCM=
+ dependencies:
+ jest-mock "^23.2.0"
+ jest-util "^23.4.0"
+ jsdom "^11.5.1"
+
+jest-environment-node@^23.4.0:
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-23.4.0.tgz#57e80ed0841dea303167cce8cd79521debafde10"
+ integrity sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=
+ dependencies:
+ jest-mock "^23.2.0"
+ jest-util "^23.4.0"
+
+jest-get-type@^22.1.0:
+ version "22.4.3"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4"
+ integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==
+
+jest-haste-map@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16"
+ integrity sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==
+ dependencies:
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.11"
+ invariant "^2.2.4"
+ jest-docblock "^23.2.0"
+ jest-serializer "^23.0.1"
+ jest-worker "^23.2.0"
+ micromatch "^2.3.11"
+ sane "^2.0.0"
+
+jest-jasmine2@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0"
+ integrity sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==
+ dependencies:
+ babel-traverse "^6.0.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^23.6.0"
+ is-generator-fn "^1.0.0"
+ jest-diff "^23.6.0"
+ jest-each "^23.6.0"
+ jest-matcher-utils "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-snapshot "^23.6.0"
+ jest-util "^23.4.0"
+ pretty-format "^23.6.0"
+
+jest-junit@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-5.2.0.tgz#980401db7aa69999cf117c6d740a8135c22ae379"
+ integrity sha512-Mdg0Qpdh1Xm/FA1B/mcLlmEmlr3XzH5pZg7MvcAwZhjHijPRd1z/UwYwkwNHmCV7o4ZOWCf77nLu7ZkhHHrtJg==
+ dependencies:
+ jest-config "^23.6.0"
+ jest-validate "^23.0.1"
+ mkdirp "^0.5.1"
+ strip-ansi "^4.0.0"
+ xml "^1.0.1"
+
+jest-leak-detector@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de"
+ integrity sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==
+ dependencies:
+ pretty-format "^23.6.0"
+
+jest-matcher-utils@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80"
+ integrity sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^22.1.0"
+ pretty-format "^23.6.0"
+
+jest-message-util@^23.4.0:
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f"
+ integrity sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=
+ dependencies:
+ "@babel/code-frame" "^7.0.0-beta.35"
+ chalk "^2.0.1"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.2.0.tgz#ad1c60f29e8719d47c26e1138098b6d18b261134"
+ integrity sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=
+
+jest-regex-util@^23.3.0:
+ version "23.3.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5"
+ integrity sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U=
+
+jest-resolve-dependencies@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d"
+ integrity sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==
+ dependencies:
+ jest-regex-util "^23.3.0"
+ jest-snapshot "^23.6.0"
+
+jest-resolve@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae"
+ integrity sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==
+ dependencies:
+ browser-resolve "^1.11.3"
+ chalk "^2.0.1"
+ realpath-native "^1.0.0"
+
+jest-runner@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38"
+ integrity sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==
+ dependencies:
+ exit "^0.1.2"
+ graceful-fs "^4.1.11"
+ jest-config "^23.6.0"
+ jest-docblock "^23.2.0"
+ jest-haste-map "^23.6.0"
+ jest-jasmine2 "^23.6.0"
+ jest-leak-detector "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-runtime "^23.6.0"
+ jest-util "^23.4.0"
+ jest-worker "^23.2.0"
+ source-map-support "^0.5.6"
+ throat "^4.0.0"
+
+jest-runtime@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082"
+ integrity sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==
+ dependencies:
+ babel-core "^6.0.0"
+ babel-plugin-istanbul "^4.1.6"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ exit "^0.1.2"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.1.11"
+ jest-config "^23.6.0"
+ jest-haste-map "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-regex-util "^23.3.0"
+ jest-resolve "^23.6.0"
+ jest-snapshot "^23.6.0"
+ jest-util "^23.4.0"
+ jest-validate "^23.6.0"
+ micromatch "^2.3.11"
+ realpath-native "^1.0.0"
+ slash "^1.0.0"
+ strip-bom "3.0.0"
+ write-file-atomic "^2.1.0"
+ yargs "^11.0.0"
+
+jest-serializer@^23.0.1:
+ version "23.0.1"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165"
+ integrity sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU=
+
+jest-snapshot@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a"
+ integrity sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==
+ dependencies:
+ babel-types "^6.0.0"
+ chalk "^2.0.1"
+ jest-diff "^23.6.0"
+ jest-matcher-utils "^23.6.0"
+ jest-message-util "^23.4.0"
+ jest-resolve "^23.6.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^23.6.0"
+ semver "^5.5.0"
+
+jest-util@^23.4.0:
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561"
+ integrity sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=
+ dependencies:
+ callsites "^2.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.11"
+ is-ci "^1.0.10"
+ jest-message-util "^23.4.0"
+ mkdirp "^0.5.1"
+ slash "^1.0.0"
+ source-map "^0.6.0"
+
+jest-validate@^23.0.1, jest-validate@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474"
+ integrity sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^22.1.0"
+ leven "^2.1.0"
+ pretty-format "^23.6.0"
+
+jest-watcher@^23.4.0:
+ version "23.4.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c"
+ integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ string-length "^2.0.0"
+
+jest-worker@^23.2.0:
+ version "23.2.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-23.2.0.tgz#faf706a8da36fae60eb26957257fa7b5d8ea02b9"
+ integrity sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=
+ dependencies:
+ merge-stream "^1.0.1"
+
+jest@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d"
+ integrity sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==
+ dependencies:
+ import-local "^1.0.0"
+ jest-cli "^23.6.0"
+
jquery-ujs@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.2.tgz#6a8ef1020e6b6dda385b90a4bddc128c21c56397"
@@ -5006,7 +5998,7 @@ js-tokens@^3.0.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-js-yaml@3.x, js-yaml@^3.12.0:
+js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==
@@ -5014,6 +6006,48 @@ js-yaml@3.x, js-yaml@^3.12.0:
argparse "^1.0.7"
esprima "^4.0.0"
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^11.5.1:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8"
+ integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==
+ dependencies:
+ abab "^2.0.0"
+ acorn "^5.5.3"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle "^1.0.0"
+ data-urls "^1.0.0"
+ domexception "^1.0.1"
+ escodegen "^1.9.1"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.3.0"
+ nwsapi "^2.0.7"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.87.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.4"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-mimetype "^2.1.0"
+ whatwg-url "^6.4.1"
+ ws "^5.2.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+ integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s=
+
jsesc@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe"
@@ -5039,21 +6073,41 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
json3@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=
-json5@^0.5.0:
+json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
jszip-utils@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/jszip-utils/-/jszip-utils-0.0.2.tgz#457d5cbca60a1c2e0706e9da2b544e8e7bc50bf8"
@@ -5202,6 +6256,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+kleur@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300"
+ integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ==
+
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
@@ -5216,6 +6275,13 @@ lazy-cache@^2.0.2:
dependencies:
set-getter "^0.1.0"
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
+ dependencies:
+ invert-kv "^1.0.0"
+
lcid@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
@@ -5223,6 +6289,16 @@ lcid@^2.0.0:
dependencies:
invert-kv "^2.0.0"
+left-pad@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
+ integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+ integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
+
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -5238,6 +6314,17 @@ lie@~3.1.0:
dependencies:
immediate "~3.0.5"
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
load-json-file@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
@@ -5333,6 +6420,11 @@ lodash.snakecase@4.1.1:
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+ integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
lodash.startcase@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8"
@@ -5343,7 +6435,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.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0:
+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==
@@ -5408,6 +6500,13 @@ make-dir@^1.0.0, make-dir@^1.3.0:
dependencies:
pify "^3.0.0"
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ dependencies:
+ tmpl "1.0.x"
+
mamacro@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
@@ -5447,6 +6546,11 @@ match-at@^0.1.1:
resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540"
integrity sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q==
+math-random@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
+ integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w=
+
md5.js@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
@@ -5460,6 +6564,13 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=
+ dependencies:
+ mimic-fn "^1.0.0"
+
mem@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf"
@@ -5494,6 +6605,18 @@ merge-source-map@^1.1.0:
dependencies:
source-map "^0.6.1"
+merge-stream@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
+ integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=
+ dependencies:
+ readable-stream "^2.0.1"
+
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+ integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+
mermaid@^8.0.0-rc.8:
version "8.0.0-rc.8"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72"
@@ -5513,7 +6636,26 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9:
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+micromatch@^3.1.4, micromatch@^3.1.6, micromatch@^3.1.8, micromatch@^3.1.9:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -5545,7 +6687,7 @@ miller-rabin@^4.0.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
-mime-types@~2.1.17, mime-types@~2.1.18:
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19:
version "2.1.21"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
@@ -5594,7 +6736,7 @@ minimist@0.0.8:
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
-minimist@1.2.0, minimist@^1.2.0:
+minimist@1.2.0, minimist@^1.1.1, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
@@ -5791,6 +6933,11 @@ node-forge@0.6.33:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
integrity sha1-RjgRh59XPUUVWtap9D3ClujoXrw=
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
@@ -5820,6 +6967,16 @@ node-forge@0.6.33:
util "^0.10.3"
vm-browserify "0.0.4"
+node-notifier@^5.2.1:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.3.0.tgz#c77a4a7b84038733d5fb351aafd8a268bfe19a01"
+ integrity sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.5.0"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
node-pre-gyp@^0.10.0:
version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
@@ -5891,7 +7048,7 @@ normalize-package-data@^2.3.2:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
-normalize-path@^2.1.1:
+normalize-path@^2.0.1, normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
@@ -5947,6 +7104,16 @@ number-is-nan@^1.0.0:
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+nwsapi@^2.0.7:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016"
+ integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==
+
+oauth-sign@~0.9.0:
+ version "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:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -6006,6 +7173,14 @@ object.getownpropertydescriptors@^2.0.3:
define-properties "^1.1.2"
es-abstract "^1.5.1"
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
object.pick@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
@@ -6120,6 +7295,15 @@ os-homedir@^1.0.0:
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
os-locale@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620"
@@ -6129,7 +7313,7 @@ os-locale@^3.0.0:
lcid "^2.0.0"
mem "^4.0.0"
-os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -6247,6 +7431,16 @@ parse-asn1@^5.0.0:
evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3"
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw=
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
parse-json@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -6262,6 +7456,11 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+ integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+
parse5@^5:
version "5.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.0.0.tgz#4d02710d44f3c3846197a11e205d4ef17842b81a"
@@ -6313,7 +7512,7 @@ path-exists@^3.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
-path-is-absolute@^1.0.0:
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
@@ -6338,6 +7537,15 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -6370,6 +7578,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -6425,6 +7638,11 @@ pluralize@^7.0.0:
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+ integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+
pofile@^1:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
@@ -6518,6 +7736,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+ integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
+
prettier@1.13.7:
version "1.13.7"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281"
@@ -6528,6 +7751,14 @@ prettier@1.15.2:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e"
integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug==
+pretty-format@^23.6.0:
+ version "23.6.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760"
+ integrity sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
prismjs@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
@@ -6535,7 +7766,7 @@ prismjs@^1.6.0:
optionalDependencies:
clipboard "^1.5.5"
-private@^0.1.6:
+private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
@@ -6565,6 +7796,14 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+prompts@^0.1.9:
+ version "0.1.14"
+ resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2"
+ integrity sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==
+ dependencies:
+ kleur "^2.0.1"
+ sisteransi "^0.1.1"
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -6595,6 +7834,11 @@ pseudomap@^1.0.2:
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+psl@^1.1.24, psl@^1.1.28:
+ version "1.1.29"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
+ integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
+
pstree.remy@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.0.tgz#f2af27265bd3e5b32bbfcc10e80bac55ba78688b"
@@ -6643,7 +7887,12 @@ punycode@1.3.2, punycode@^1.2.4:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
-punycode@^2.1.0:
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -6658,6 +7907,11 @@ qs@6.5.1:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
query-string@^5.0.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
@@ -6682,6 +7936,15 @@ querystringify@^2.0.0:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef"
integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==
+randomatic@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
+ integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
@@ -6739,6 +8002,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
@@ -6755,6 +8026,15 @@ read-pkg-up@^4.0.0:
find-up "^3.0.0"
read-pkg "^3.0.0"
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
read-pkg@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
@@ -6808,6 +8088,13 @@ readdirp@^2.0.0:
readable-stream "^2.0.2"
set-immediate-shim "^1.0.1"
+realpath-native@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560"
+ integrity sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==
+ dependencies:
+ util.promisify "^1.0.0"
+
regenerate-unicode-properties@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c"
@@ -6837,6 +8124,13 @@ regenerator-transform@^0.13.3:
dependencies:
private "^0.1.6"
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -6925,11 +8219,60 @@ repeat-string@^0.2.2:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4=
-repeat-string@^1.6.1:
+repeat-string@^1.5.2, repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+ dependencies:
+ is-finite "^1.0.0"
+
+request-promise-core@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
+ integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
+ dependencies:
+ lodash "^4.13.1"
+
+request-promise-native@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5"
+ integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=
+ dependencies:
+ request-promise-core "1.1.1"
+ stealthy-require "^1.1.0"
+ tough-cookie ">=2.3.3"
+
+request@^2.87.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -6975,7 +8318,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@1.1.x:
+resolve@1.1.7, resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
@@ -7027,6 +8370,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0"
inherits "^2.0.1"
+rsvp@^3.3.3:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
+ integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==
+
run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@@ -7075,11 +8423,27 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
-"safer-buffer@>= 2.1.2 < 3":
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+sane@^2.0.0:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa"
+ integrity sha1-tNwYYcIbQn6SlQej51HiosuKs/o=
+ dependencies:
+ anymatch "^2.0.0"
+ capture-exit "^1.2.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.18.0"
+ optionalDependencies:
+ fsevents "^1.2.3"
+
sanitize-html@^1.16.1:
version "1.16.3"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56"
@@ -7285,6 +8649,11 @@ shebang-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+ integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
sigmund@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@@ -7295,6 +8664,16 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+sisteransi@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce"
+ integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g==
+
+slash@^1.0.0:
+ version "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"
@@ -7307,12 +8686,12 @@ slugify@^1.3.1:
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.1.tgz#f572127e8535329fbc6c1edb74ab856b61ad7de2"
integrity sha512-6BwyhjF5tG5P8s+0DPNyJmBSBePG6iMyhjvIW5zGdA3tFik9PtK+yNkZgTeiroCRGZYgkHftFA62tGVK1EI9Kw==
-smooshpack@^0.0.48:
- version "0.0.48"
- resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.48.tgz#6fbeaaf59226a1fe500f56aa17185eed377d2823"
- integrity sha512-BmaIr6fK6w7WBCI4V7tcZIg78WeE6X56zrhGSNk5vXavT1bQPXH+brZFq6Hwi833upY/yusG2FMVkf7TZsVv/w==
+smooshpack@^0.0.53:
+ version "0.0.53"
+ resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.53.tgz#aa397ca43619912e8ac0aa32012846ff4feaa5e8"
+ integrity sha512-FVXvKvZOz5+Tk/zUJ/wxM+ftu1yZtFEmeQl4chCqbzK/reU0L/OdDiYpx+/27Jt2VX09j08oIzwoyQ5fHH4+WQ==
dependencies:
- codesandbox-api "^0.0.18"
+ codesandbox-api "^0.0.20"
codesandbox-import-utils "^1.2.3"
lodash.isequal "^4.5.0"
@@ -7440,6 +8819,21 @@ source-map-resolve@^0.5.0:
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"
+ integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-support@^0.5.6:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+ integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
source-map-url@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
@@ -7450,12 +8844,12 @@ source-map@0.5.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
integrity sha1-D+llA6yGpa213mP05BKuSHLNvoY=
-source-map@^0.5.0, source-map@^0.5.6:
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
-source-map@^0.6.1, source-map@~0.6.1:
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -7541,6 +8935,21 @@ srcset@^1.0.0:
array-uniq "^1.0.2"
number-is-nan "^1.0.0"
+sshpk@^1.7.0:
+ version "1.15.2"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
+ integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
ssri@^5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.4.tgz#9985e14041e65fc397af96542be35724ac11da52"
@@ -7555,6 +8964,11 @@ ssri@^6.0.0:
dependencies:
figgy-pudding "^3.5.1"
+stack-utils@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+ integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -7573,6 +8987,11 @@ statuses@~1.3.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=
+stealthy-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+ integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
stickyfilljs@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.0.5.tgz#d229e372d2199ddf5d283bbe34ac1f7d2529c2fc"
@@ -7632,6 +9051,14 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -7675,11 +9102,18 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
-strip-bom@^3.0.0:
+strip-bom@3.0.0, strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+ dependencies:
+ is-utf8 "^0.2.0"
+
strip-css-comments@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-css-comments/-/strip-css-comments-3.0.0.tgz#7a5625eff8a2b226cf8947a11254da96e13dae89"
@@ -7710,7 +9144,7 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-supports-color@^3.1.0:
+supports-color@^3.1.0, supports-color@^3.1.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=
@@ -7734,6 +9168,11 @@ symbol-observable@^1.0.2:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
+symbol-tree@^3.2.2:
+ version "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"
@@ -7776,6 +9215,17 @@ term-size@^1.2.0:
dependencies:
execa "^0.7.0"
+test-exclude@^4.2.1:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20"
+ integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
test-exclude@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.0.0.tgz#cdce7cece785e0e829cd5c2b27baf18bc583cfb7"
@@ -7811,6 +9261,11 @@ three@^0.84.0:
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
integrity sha1-lb6FpVoPoAKqYl7VWRMJV9z/2Rg=
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+ integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
+
throttle-debounce@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.0.1.tgz#7307ddd6cd9acadb349132fbf6c18d78c88a5e62"
@@ -7865,6 +9320,11 @@ tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+ integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@@ -7922,6 +9382,29 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
+tough-cookie@>=2.3.3, tough-cookie@^2.3.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+ integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+ dependencies:
+ psl "^1.1.28"
+ punycode "^2.1.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+tr46@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+ dependencies:
+ punycode "^2.1.0"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -7942,6 +9425,18 @@ tty-browserify@0.0.0:
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -8237,6 +9732,15 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
@@ -8343,6 +9847,28 @@ vuex@^3.0.1:
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
integrity sha512-wLoqz0B7DSZtgbWL1ShIBBCjv22GV5U+vcBFox658g6V0s4wZV9P4YjCNyoHSyIBpj1f29JBoNQIqD82cR4O3w==
+w3c-hr-time@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+ integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"
+ integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY=
+ dependencies:
+ exec-sh "^0.2.0"
+ minimist "^1.2.0"
+
watchpack@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
@@ -8359,6 +9885,11 @@ wbuf@^1.1.0, wbuf@^1.7.2:
dependencies:
minimalistic-assert "^1.0.0"
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
webpack-bundle-analyzer@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.2.tgz#22f19ea6d1b5a15fd7a90baae0bc0f39bd1e4d48"
@@ -8501,6 +10032,36 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7"
integrity sha1-domUmcGEtu91Q3fC27DNbLVdKec=
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+ integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
+ dependencies:
+ iconv-lite "0.4.24"
+
+whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+ integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+
+whatwg-url@^6.4.1:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
+ integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
+whatwg-url@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+ integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.1"
+ webidl-conversions "^4.0.2"
+
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -8513,6 +10074,13 @@ which@^1.1.1, which@^1.2.1, which@^1.2.9:
dependencies:
isexe "^2.0.0"
+which@^1.2.12, which@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
wide-align@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
@@ -8566,7 +10134,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-write-file-atomic@^2.0.0:
+write-file-atomic@^2.0.0, write-file-atomic@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==
@@ -8582,6 +10150,13 @@ write@^0.2.1:
dependencies:
mkdirp "^0.5.1"
+ws@^5.2.0:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
+ integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+ dependencies:
+ async-limiter "~1.0.0"
+
ws@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.0.0.tgz#eaa494aded00ac4289d455bac8d84c7c651cef35"
@@ -8603,6 +10178,16 @@ xdg-basedir@^3.0.0:
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+ integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+
+xml@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
+ integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
+
xmlbuilder@8.2.2:
version "8.2.2"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773"
@@ -8633,6 +10218,11 @@ xterm@^3.5.0:
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.5.0.tgz#ba3f464bc5730c9d259ebe62131862224db9ddcc"
integrity sha512-IpG3P3gkT0/xDPS0j3igpk92JYlUajaEHk3/EQSUeIRJmPiF2lyham3Xt/GD3o98uOrRluvowjNj0AFeYK+AXQ==
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+ integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
+
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
@@ -8655,6 +10245,13 @@ yargs-parser@^10.1.0:
dependencies:
camelcase "^4.1.0"
+yargs-parser@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
+ integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=
+ dependencies:
+ camelcase "^4.1.0"
+
yargs@12.0.2, yargs@^12.0.1:
version "12.0.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc"
@@ -8673,6 +10270,24 @@ yargs@12.0.2, yargs@^12.0.1:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^10.1.0"
+yargs@^11.0.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"
+ integrity sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.1.1"
+ find-up "^2.1.0"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^9.0.2"
+
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
@@ -8689,3 +10304,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==