summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock15
-rw-r--r--Gemfile.rails5.lock15
-rw-r--r--app/assets/images/cluster_app_logos/knative.pngbin0 -> 11259 bytes
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue142
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue90
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue48
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/models/issue.js1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js15
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js24
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue71
-rw-r--r--app/assets/javascripts/clusters/constants.js8
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js4
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue7
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue72
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue55
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue2
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue139
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue10
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/store/actions.js19
-rw-r--r--app/assets/javascripts/diffs/store/getters.js5
-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.js53
-rw-r--r--app/assets/javascripts/diffs/store/utils.js20
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue9
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue7
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue4
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue24
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue19
-rw-r--r--app/assets/javascripts/jobs/mixins/delayed_job_mixin.js50
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js12
-rw-r--r--app/assets/javascripts/merge_request_tabs.js3
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue36
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue30
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue71
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue27
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue165
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue94
-rw-r--r--app/assets/javascripts/pages/groups/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/clusters/update/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/registry/components/app.vue2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue2
-rw-r--r--app/assets/javascripts/users_select.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue37
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue79
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue14
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss42
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/boards.scss197
-rw-r--r--app/assets/stylesheets/pages/clusters.scss35
-rw-r--r--app/assets/stylesheets/pages/diff.scss50
-rw-r--r--app/assets/stylesheets/pages/note_form.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss293
-rw-r--r--app/controllers/boards/issues_controller.rb16
-rw-r--r--app/controllers/chaos_controller.rb56
-rw-r--r--app/controllers/clusters/clusters_controller.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/concerns/send_file_upload.rb4
-rw-r--r--app/controllers/groups/clusters/applications_controller.rb18
-rw-r--r--app/controllers/groups/clusters_controller.rb25
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb24
-rw-r--r--app/controllers/projects/artifacts_controller.rb2
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb18
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/finders/snippets_finder.rb209
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/avatars_helper.rb2
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/events_helper.rb9
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/profiles_helper.rb14
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/models/application_setting.rb13
-rw-r--r--app/models/ci/build.rb31
-rw-r--r--app/models/clusters/applications/knative.rb51
-rw-r--r--app/models/clusters/cluster.rb13
-rw-r--r--app/models/clusters/platforms/kubernetes.rb18
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/compare.rb11
-rw-r--r--app/models/concerns/each_batch.rb17
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/environment.rb1
-rw-r--r--app/models/environment_status.rb44
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/merge_request_diff.rb7
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_import_state.rb13
-rw-r--r--app/models/repository.rb12
-rw-r--r--app/models/snippet.rb77
-rw-r--r--app/models/upload.rb18
-rw-r--r--app/models/user.rb30
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/policies/clusters/cluster_policy.rb6
-rw-r--r--app/policies/group_policy.rb4
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/clusterable_presenter.rb12
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/group_clusterable_presenter.rb36
-rw-r--r--app/presenters/project_clusterable_presenter.rb16
-rw-r--r--app/serializers/README.md4
-rw-r--r--app/serializers/issue_board_entity.rb51
-rw-r--r--app/serializers/issue_serializer.rb6
-rw-r--r--app/serializers/label_entity.rb4
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/clusters/applications/create_service.rb14
-rw-r--r--app/services/clusters/applications/install_service.rb6
-rw-r--r--app/services/clusters/create_service.rb4
-rw-r--r--app/services/commits/change_service.rb8
-rw-r--r--app/services/commits/commit_patch_service.rb61
-rw-r--r--app/services/commits/create_service.rb7
-rw-r--r--app/services/issuable_base_service.rb4
-rw-r--r--app/services/merge_requests/build_service.rb6
-rw-r--r--app/services/merge_requests/get_urls_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb12
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/services/submodules/update_service.rb38
-rw-r--r--app/views/abuse_reports/new.html.haml12
-rw-r--r--app/views/admin/application_settings/_email.html.haml6
-rw-r--r--app/views/clusters/clusters/_buttons.html.haml4
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml40
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml6
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml9
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml7
-rw-r--r--app/views/clusters/clusters/index.html.haml7
-rw-r--r--app/views/clusters/clusters/show.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml7
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml7
-rw-r--r--app/views/dashboard/snippets/index.html.haml5
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml15
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml7
-rw-r--r--app/views/profiles/show.html.haml5
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml5
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml3
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb46
-rw-r--r--changelogs/unreleased/21480-parallel-job-keyword-mvc.yml5
-rw-r--r--changelogs/unreleased/34758-group-cluster-controller.yml5
-rw-r--r--changelogs/unreleased/43521-keep-personal-emails-private.yml5
-rw-r--r--changelogs/unreleased/47008-issue-board-card-design.yml5
-rw-r--r--changelogs/unreleased/51259-ci-cd-tooltips.yml6
-rw-r--r--changelogs/unreleased/52767-more-chaos-for-gitlab.yml5
-rw-r--r--changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml5
-rw-r--r--changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml4
-rw-r--r--changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml5
-rw-r--r--changelogs/unreleased/add-action-to-deployment.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml5
-rw-r--r--changelogs/unreleased/bvl-patches-via-mail.yml5
-rw-r--r--changelogs/unreleased/diff-expand-all-button.yml5
-rw-r--r--changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml5
-rw-r--r--changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml5
-rw-r--r--changelogs/unreleased/fj-41213-api-update-submodule-commit.yml5
-rw-r--r--changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml5
-rw-r--r--changelogs/unreleased/fj-bump-gitaly-0-129-0.yml5
-rw-r--r--changelogs/unreleased/gl-ui-loading-icon.yml5
-rw-r--r--changelogs/unreleased/gt-align-sign-in-button.yml5
-rw-r--r--changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml5
-rw-r--r--changelogs/unreleased/introduce-knative-support.yml5
-rw-r--r--changelogs/unreleased/max_retries_when.yml5
-rw-r--r--changelogs/unreleased/mr-image-commenting.yml5
-rw-r--r--changelogs/unreleased/refactor-snippets-finder.yml5
-rw-r--r--changelogs/unreleased/remove-asset-sync.yml5
-rw-r--r--changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml5
-rw-r--r--changelogs/unreleased/rs-revert-api.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-52649.yml5
-rw-r--r--changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml5
-rw-r--r--changelogs/unreleased/tc-index-uploads-file-store.yml5
-rw-r--r--changelogs/unreleased/upgrade-workhorse-7-1-0.yml5
-rw-r--r--changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml5
-rw-r--r--config/initializers/asset_sync.rb31
-rw-r--r--config/initializers/fill_shards.rb7
-rw-r--r--config/routes.rb7
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/routes/project.rb5
-rw-r--r--danger/metadata/Dangerfile7
-rw-r--r--db/fixtures/development/09_issues.rb3
-rw-r--r--db/migrate/20180912111628_add_knative_application.rb20
-rw-r--r--db/migrate/20181005125926_add_index_to_uploads_store.rb17
-rw-r--r--db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb11
-rw-r--r--db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb42
-rw-r--r--db/migrate/20181106135939_add_index_to_deployments.rb17
-rw-r--r--db/post_migrate/20181105201455_steal_fill_store_upload.rb31
-rw-r--r--db/post_migrate/20181107054254_remove_restricted_todos_again.rb32
-rw-r--r--db/schema.rb18
-rw-r--r--doc/administration/auth/okta.md2
-rw-r--r--doc/administration/container_registry.md5
-rw-r--r--doc/administration/custom_hooks.md2
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/commits.md42
-rw-r--r--doc/api/merge_requests.md55
-rw-r--r--doc/api/repositories.md1
-rw-r--r--doc/api/repository_submodules.md49
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md84
-rw-r--r--doc/development/chaos_endpoints.md117
-rw-r--r--doc/development/contributing/issue_workflow.md4
-rw-r--r--doc/development/documentation/index.md115
-rw-r--r--doc/development/documentation/styleguide.md64
-rw-r--r--doc/development/documentation/workflow.md33
-rw-r--r--doc/development/performance.md3
-rw-r--r--doc/development/testing_guide/review_apps.md92
-rw-r--r--doc/development/utilities.md4
-rw-r--r--doc/install/database_mysql.md48
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/install/kubernetes/gitlab_chart.md2
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/raketasks/import.md72
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/university/README.md11
-rw-r--r--doc/university/training/topics/additional_resources.md12
-rw-r--r--doc/university/training/topics/tags.md2
-rw-r--r--doc/university/training/user_training.md2
-rw-r--r--doc/update/11.4-to-11.5.md28
-rw-r--r--doc/user/admin_area/settings/email.md17
-rw-r--r--doc/user/profile/index.md40
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md5
-rw-r--r--doc/user/project/clusters/index.md132
-rw-r--r--doc/user/project/merge_requests/index.md17
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commits.rb34
-rw-r--r--lib/api/entities.rb32
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/submodules.rb47
-rw-r--r--lib/bitbucket_server/client.rb8
-rw-r--r--lib/bitbucket_server/collection.rb24
-rw-r--r--lib/bitbucket_server/paginator.rb27
-rw-r--r--lib/gitlab/background_migration/remove_restricted_todos.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb24
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb90
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb24
-rw-r--r--lib/gitlab/ci/config/normalizer.rb65
-rw-r--r--lib/gitlab/ci/status/build/scheduled.rb13
-rw-r--r--lib/gitlab/ci/yaml_processor.rb4
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb50
-rw-r--r--lib/gitlab/email/receiver.rb1
-rw-r--r--lib/gitlab/git/patches/collection.rb33
-rw-r--r--lib/gitlab/git/patches/commit_patches.rb31
-rw-r--r--lib/gitlab/git/patches/patch.rb19
-rw-r--r--lib/gitlab/git/repository.rb14
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb49
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb29
-rw-r--r--lib/gitlab/kubernetes/helm/upgrade_command.rb4
-rw-r--r--lib/gitlab/private_commit_email.rb28
-rw-r--r--lib/gitlab/quick_actions/extractor.rb14
-rw-r--r--lib/gitlab/usage_data.rb19
-rw-r--r--locale/gitlab.pot109
-rw-r--r--qa/qa/git/repository.rb26
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/runtime/env.rb19
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb49
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb84
-rw-r--r--qa/qa/specs/runner.rb4
-rw-r--r--qa/spec/git/repository_spec.rb30
-rw-r--r--qa/spec/runtime/env_spec.rb47
-rw-r--r--qa/spec/specs/runner_spec.rb14
-rwxr-xr-xscripts/review_apps/review-apps.sh3
-rwxr-xr-xscripts/trigger-build3
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb6
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb29
-rw-r--r--spec/controllers/groups/clusters/applications_controller_spec.rb87
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb574
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb11
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb33
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb2
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb152
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/factories/clusters/applications/helm.rb5
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb2
-rw-r--r--spec/factories/merge_request_diff_files.rb47
-rw-r--r--spec/factories/merge_request_diffs.rb13
-rw-r--r--spec/factories/services.rb11
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/issue_ordering_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/groups_spec.rb22
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb9
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb62
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb4
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb11
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/merge_requests/user_squashes_merge_request_spec.rb4
-rw-r--r--spec/features/projects/clusters_spec.rb31
-rw-r--r--spec/features/projects/environments/environment_spec.rb41
-rw-r--r--spec/features/projects/environments/environments_spec.rb18
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb5
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb4
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb6
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/merge_request_button_spec.rb8
-rw-r--r--spec/finders/snippets_finder_spec.rb45
-rw-r--r--spec/fixtures/api/schemas/entities/issue_board.json1
-rw-r--r--spec/fixtures/api/schemas/entities/issue_boards.json15
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json26
-rw-r--r--spec/fixtures/emails/merge_request_multiple_patches.eml181
-rw-r--r--spec/fixtures/emails/merge_request_with_conflicting_patch.eml45
-rw-r--r--spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml44
-rw-r--r--spec/fixtures/emails/valid_merge_request_with_patch.eml151
-rw-r--r--spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch19
-rw-r--r--spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch23
-rw-r--r--spec/helpers/events_helper_spec.rb46
-rw-r--r--spec/helpers/profiles_helper_spec.rb29
-rw-r--r--spec/javascripts/boards/components/issue_due_date_spec.js64
-rw-r--r--spec/javascripts/boards/components/issue_time_estimate_spec.js40
-rw-r--r--spec/javascripts/boards/issue_card_spec.js25
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js12
-rw-r--r--spec/javascripts/clusters/services/mock_data.js11
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js8
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js59
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js78
-rw-r--r--spec/javascripts/diffs/components/image_diff_overlay_spec.js146
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js21
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js1
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js2
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js8
-rw-r--r--spec/javascripts/fixtures/jobs.rb23
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js35
-rw-r--r--spec/javascripts/jobs/components/job_container_item_spec.js26
-rw-r--r--spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js93
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js6
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js2
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js16
-rw-r--r--spec/javascripts/notes/components/toggle_replies_widget_spec.js78
-rw-r--r--spec/javascripts/pipelines/graph/job_item_spec.js27
-rw-r--r--spec/javascripts/pipelines/header_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js5
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js57
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js28
-rw-r--r--spec/lib/bitbucket_server/client_spec.rb12
-rw-r--r--spec/lib/bitbucket_server/collection_spec.rb29
-rw-r--r--spec/lib/bitbucket_server/paginator_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/entry/retry_spec.rb236
-rw-r--r--spec/lib/gitlab/ci/config/normalizer_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/status/build/scheduled_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb31
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb69
-rw-r--r--spec/lib/gitlab/git/patches/collection_spec.rb28
-rw-r--r--spec/lib/gitlab/git/patches/commit_patches_spec.rb49
-rw-r--r--spec/lib/gitlab/git/patches/patch_spec.rb16
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb33
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb15
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb76
-rw-r--r--spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb16
-rw-r--r--spec/lib/gitlab/private_commit_email_spec.rb41
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb19
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb10
-rw-r--r--spec/migrations/steal_fill_store_upload_spec.rb40
-rw-r--r--spec/models/application_setting_spec.rb11
-rw-r--r--spec/models/ci/build_spec.rb132
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb24
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb24
-rw-r--r--spec/models/clusters/applications/knative_spec.rb77
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb24
-rw-r--r--spec/models/clusters/applications/runner_spec.rb24
-rw-r--r--spec/models/clusters/cluster_spec.rb25
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb12
-rw-r--r--spec/models/compare_spec.rb29
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/concerns/each_batch_spec.rb51
-rw-r--r--spec/models/environment_spec.rb19
-rw-r--r--spec/models/environment_status_spec.rb88
-rw-r--r--spec/models/merge_request_diff_spec.rb13
-rw-r--r--spec/models/merge_request_spec.rb38
-rw-r--r--spec/models/project_spec.rb22
-rw-r--r--spec/models/snippet_spec.rb211
-rw-r--r--spec/models/upload_spec.rb62
-rw-r--r--spec/models/user_spec.rb79
-rw-r--r--spec/models/wiki_page_spec.rb23
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb42
-rw-r--r--spec/policies/group_policy_spec.rb6
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb7
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb77
-rw-r--r--spec/requests/api/commits_spec.rb112
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/submodules_spec.rb99
-rw-r--r--spec/serializers/environment_status_entity_spec.rb1
-rw-r--r--spec/serializers/issue_board_entity_spec.rb23
-rw-r--r--spec/serializers/issue_serializer_spec.rb8
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb32
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb59
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb2
-rw-r--r--spec/services/clusters/update_service_spec.rb27
-rw-r--r--spec/services/commits/commit_patch_service_spec.rb92
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb8
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb9
-rw-r--r--spec/services/submodules/update_service_spec.rb212
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb14
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb4
-rw-r--r--spec/support/helpers/migrations_helpers.rb22
-rw-r--r--spec/support/helpers/test_env.rb11
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb4
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb25
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb7
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb8
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb8
-rw-r--r--spec/workers/email_receiver_worker_spec.rb15
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb22
-rw-r--r--vendor/gitignore/Android.gitignore4
-rw-r--r--vendor/gitignore/Delphi.gitignore3
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/Images.gitignore63
-rw-r--r--vendor/gitignore/Global/NetBeans.gitignore2
-rw-r--r--vendor/gitignore/Global/PSoCCreator.gitignore18
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore74
-rw-r--r--vendor/gitignore/Laravel.gitignore2
-rw-r--r--vendor/gitignore/Magento.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore3
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Rails.gitignore1
-rw-r--r--vendor/gitignore/Unity.gitignore1
-rw-r--r--vendor/licenses.csv52
532 files changed, 10233 insertions, 2250 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0e7a67f9cc1..407cd8696a2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -954,7 +954,6 @@ review:
- download_gitlab_chart
- ensure_namespace
- install_tiller
- - create_secret
- install_external_dns
- deploy
environment:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 4c2a8041846..4db8830b115 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.128.0
+0.129.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4122521804f..a3fcc7121bb 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-7.0.0 \ No newline at end of file
+7.1.0
diff --git a/Gemfile b/Gemfile
index 6674edc1d0c..c7efa790cfd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -416,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.123.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
@@ -431,6 +431,3 @@ gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
-
-# Asset synchronization
-gem 'asset_sync', '~> 2.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index e755b0e0a8d..50e3ddef1e1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,11 +58,6 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.4.0)
- activemodel (>= 4.1.0)
- fog-core
- mime-types (>= 2.99)
- unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.1.0)
@@ -274,9 +269,8 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
@@ -939,7 +933,6 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -1000,7 +993,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
@@ -1158,4 +1151,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 6ae7444f18f..181f2db95b0 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -61,11 +61,6 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.4.0)
- activemodel (>= 4.1.0)
- fog-core
- mime-types (>= 2.99)
- unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.1.0)
@@ -277,9 +272,8 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
@@ -948,7 +942,6 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print
babosa (~> 1.0.2)
@@ -1009,7 +1002,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
@@ -1167,4 +1160,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png
new file mode 100644
index 00000000000..0a2510c8549
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/knative.png
Binary files differ
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 97232d7f783..8512bf9dd7b 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -1,12 +1,14 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'Badge',
components: {
Icon,
Tooltip,
+ GlLoadingIcon,
},
directives: {
Tooltip,
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index aff7c4180e3..47e6e618219 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -14,6 +15,7 @@ export default {
components: {
Badge,
LoadingButton,
+ GlLoadingIcon,
},
props: {
isEditing: {
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 359d3e10380..ab518820378 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,5 +1,6 @@
<script>
import { mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
@@ -7,6 +8,7 @@ export default {
name: 'BadgeList',
components: {
BadgeListRow,
+ GlLoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index 5d16ba3ce6d..f28eff18f03 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
@@ -10,6 +11,7 @@ export default {
components: {
Badge,
Icon,
+ GlLoadingIcon,
},
props: {
badge: {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 4dc56c670f0..5e28fc396ab 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,5 +1,6 @@
<script>
import Sortable from 'sortablejs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
@@ -11,6 +12,7 @@ export default {
components: {
boardCard,
boardNewIssue,
+ GlLoadingIcon,
},
props: {
groupId: {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d956777a86b..2315a48a306 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,18 +1,24 @@
<script>
-import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
-import tooltip from '../../vue_shared/directives/tooltip';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
export default {
components: {
- UserAvatarLink,
Icon,
+ UserAvatarLink,
+ TooltipOnTruncate,
+ IssueDueDate,
+ IssueTimeEstimate,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
issue: {
@@ -45,8 +51,8 @@ export default {
},
data() {
return {
- limitBeforeCounter: 3,
- maxRender: 4,
+ limitBeforeCounter: 2,
+ maxRender: 3,
maxCounter: 99,
};
},
@@ -55,7 +61,9 @@ export default {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
+ const { numberOverLimit, maxCounter } = this;
+ const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
+ return sprintf(__('%{count} more assignees'), { count });
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
@@ -80,6 +88,10 @@ export default {
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
+ issueReferencePath() {
+ const { referencePath, groupId } = this.issue;
+ return !groupId ? referencePath.split('#')[0] : null;
+ },
},
methods: {
isIndexLessThanlimit(index) {
@@ -96,11 +108,9 @@ export default {
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
+ if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
},
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
@@ -108,19 +118,29 @@ export default {
if (!label.id) return false;
return true;
},
- filterByLabel(label, e) {
+ filterByLabel(label) {
+ if (!this.updateFilters) return;
+ const labelTitle = encodeURIComponent(label.title);
+ const filter = `label_name[]=${labelTitle}`;
+
+ this.applyFilter(filter);
+ },
+ filterByWeight(weight) {
if (!this.updateFilters) return;
+ const issueWeight = encodeURIComponent(weight);
+ const filter = `weight=${issueWeight}`;
+
+ this.applyFilter(filter);
+ },
+ applyFilter(filter) {
const filterPath = boardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
+ const filterIndex = filterPath.indexOf(filter);
- if (labelIndex === -1) {
- filterPath.push(param);
+ if (filterIndex === -1) {
+ filterPath.push(filter);
} else {
- filterPath.splice(labelIndex, 1);
+ filterPath.splice(filterIndex, 1);
}
boardsStore.filter.path = filterPath.join('&');
@@ -141,24 +161,62 @@ export default {
<template>
<div>
<div class="board-card-header">
- <h4 class="board-card-title">
+ <h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
+ v-gl-tooltip
name="eye-slash"
- class="confidential-icon"
- />
- <a
+ :title="__('Confidential')"
+ class="confidential-icon append-right-4"
+ :aria-label="__('Confidential')"
+ /><a
:href="issue.path"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
+ </h4>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-labels prepend-top-4 d-flex flex-wrap"
+ >
+ <button
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ v-gl-tooltip
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label append-right-4 prepend-top-4"
+ type="button"
+ @click="filterByLabel(label)"
+ >
+ {{ label.title }}
+ </button>
+ </div>
+ <div class="board-card-footer d-flex justify-content-between align-items-end">
+ <div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
<span
- v-if="issueId"
- class="board-card-number append-right-5"
+ v-if="issue.referencePath"
+ class="board-card-number d-flex append-right-8 prepend-top-8"
>
- {{ issue.referencePath }}
+ <tooltip-on-truncate
+ v-if="issueReferencePath"
+ :title="issueReferencePath"
+ placement="bottom"
+ class="board-issue-path block-truncated bold"
+ >{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
- </h4>
+ <span class="board-info-items prepend-top-8 d-inline-block">
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ /><issue-time-estimate
+ v-if="issue.timeEstimate"
+ :estimate="issue.timeEstimate"
+ />
+ </span>
+ </div>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
@@ -167,38 +225,26 @@ export default {
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
+ :img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
- />
+ >
+ <span class="js-assignee-tooltip">
+ <span class="bold d-block">Assignee</span>
+ {{ assignee.name }}
+ <span class="text-white-50">@{{ assignee.username }}</span>
+ </span>
+ </user-avatar-link>
<span
v-if="shouldRenderCounter"
- v-tooltip
+ v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
+ data-placement="bottom"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
- <div
- v-if="showLabelFooter"
- class="board-card-footer"
- >
- <button
- v-for="label in issue.labels"
- v-if="showLabel(label)"
- :key="label.id"
- v-tooltip
- :style="labelStyle(label)"
- :title="label.description"
- class="badge color-label"
- type="button"
- data-container="body"
- @click="filterByLabel(label, $event)"
- >
- {{ label.title }}
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
new file mode 100644
index 00000000000..025ef7e9743
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -0,0 +1,90 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ date: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ const timeago = getTimeago();
+ const { timeDifference, standardDateFormat } = this;
+ const formatedDate = standardDateFormat;
+
+ if (timeDifference >= -1 && timeDifference < 7) {
+ return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+ }
+
+ return timeago.format(this.issueDueDate);
+ },
+ body() {
+ const { timeDifference, issueDueDate, standardDateFormat } = this;
+
+ if (timeDifference === 0) {
+ return __('Today');
+ } else if (timeDifference === 1) {
+ return __('Tomorrow');
+ } else if (timeDifference === -1) {
+ return __('Yesterday');
+ } else if (timeDifference > 0 && timeDifference < 7) {
+ return dateFormat(issueDueDate, 'dddd', true);
+ }
+
+ return standardDateFormat;
+ },
+ issueDueDate() {
+ return new Date(this.date);
+ },
+ timeDifference() {
+ const today = new Date();
+ return getDayDifference(today, this.issueDueDate);
+ },
+ isPastDue() {
+ if (this.timeDifference >= 0) return false;
+ return true;
+ },
+ standardDateFormat() {
+ const today = new Date();
+ const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
+
+ return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueDueDate"
+ class="board-card-info card-number"
+ >
+ <icon
+ :class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
+ name="calendar"
+ /><time
+ :class="{'text-danger': isPastDue}"
+ datetime="date"
+ class="board-card-info-text">{{ body }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueDueDate"
+ placement="bottom"
+ >
+ <span class="bold">{{ __('Due date') }}</span>
+ <br />
+ <span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
new file mode 100644
index 00000000000..efc7daf7812
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ estimate: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return stringifyTime(parseSeconds(this.estimate), true);
+ },
+ timeEstimate() {
+ return stringifyTime(parseSeconds(this.estimate));
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueTimeEstimate"
+ class="board-card-info card-number"
+ >
+ <icon
+ name="hourglass"
+ css-classes="board-card-info-icon"
+ /><time class="board-card-info-text">{{ timeEstimate }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueTimeEstimate"
+ placement="bottom"
+ class="js-issue-time-estimate"
+ >
+ <span class="bold d-block">{{ __('Time estimate') }}</span>
+ {{ title }}
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 40949cc0656..fdd1346d4c7 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -6,6 +6,7 @@ import ModalList from './list.vue';
import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
@@ -13,6 +14,7 @@ export default {
ModalHeader,
ModalList,
ModalFooter,
+ GlLoadingIcon,
},
props: {
newIssuePath: {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 0f01a2a6c09..503417644fa 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import Api from '../../api';
@@ -9,6 +10,7 @@ export default {
name: 'BoardProjectSelect',
components: {
Icon,
+ GlLoadingIcon,
},
props: {
groupId: {
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 669630edcab..5e0f0b07247 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -30,6 +30,7 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
+ this.timeEstimate = obj.time_estimate;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index ebf76af5966..71fc2ac7d80 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
-import applications from './components/applications.vue';
+import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/**
@@ -28,8 +28,10 @@ export default class Clusters {
installIngressPath,
installRunnerPath,
installJupyterPath,
+ installKnativePath,
installPrometheusPath,
managePrometheusPath,
+ clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
@@ -49,6 +51,7 @@ export default class Clusters {
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
+ installKnativeEndpoint: installKnativePath,
});
this.installApplication = this.installApplication.bind(this);
@@ -65,7 +68,7 @@ export default class Clusters {
initDismissableCallout('.js-cluster-security-warning');
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
- this.initApplications();
+ this.initApplications(clusterType);
if (this.store.state.status !== 'created') {
this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
@@ -77,23 +80,21 @@ export default class Clusters {
}
}
- initApplications() {
+ initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
- components: {
- applications,
- },
data() {
return {
state: store.state,
};
},
render(createElement) {
- return createElement('applications', {
+ return createElement(Applications, {
props: {
+ type,
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
deleted file mode 100644
index 789c8360124..00000000000
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import setupToggleButtons from '~/toggle_buttons';
-import initDismissableCallout from '~/dismissable_callout';
-
-import ClustersService from './services/clusters_service';
-
-export default () => {
- const clusterList = document.querySelector('.js-clusters-list');
-
- initDismissableCallout('.gcp-signup-offer');
-
- // The empty state won't have a clusterList
- if (clusterList) {
- setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
- ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
- err => {
- createFlash(__('Something went wrong on our end.'));
- throw err;
- },
- ),
- );
- }
-};
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 6d7f45a35d8..c1026d1273a 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -7,12 +7,13 @@ import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
+import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { APPLICATION_STATUS, INGRESS } from '../constants';
+import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@@ -20,6 +21,11 @@ export default {
clipboardButton,
},
props: {
+ type: {
+ type: String,
+ required: false,
+ default: CLUSTER_TYPE.PROJECT,
+ },
applications: {
type: Object,
required: false,
@@ -53,10 +59,14 @@ export default {
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
+ knativeLogo,
meltanoLogo,
prometheusLogo,
}),
computed: {
+ isProjectCluster() {
+ return this.type === CLUSTER_TYPE.PROJECT;
+ },
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
@@ -136,6 +146,9 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ knativeInstalled() {
+ return this.applications.knative.status === APPLICATION_STATUS.INSTALLED;
+ },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
@@ -276,6 +289,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title"
@@ -294,6 +308,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
@@ -312,6 +327,7 @@ export default {
</div>
</application-row>
<application-row
+ v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
@@ -321,7 +337,6 @@ export default {
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
@@ -371,6 +386,58 @@ export default {
</template>
</div>
</application-row>
+ <application-row
+ id="knative"
+ :logo-url="knativeLogo"
+ :title="applications.knative.title"
+ :status="applications.knative.status"
+ :status-reason="applications.knative.statusReason"
+ :request-status="applications.knative.requestStatus"
+ :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|A Knative build extends Kubernetes
+ and utilizes existing Kubernetes primitives to provide you with
+ the ability to run on-cluster container builds from source.
+ For example, you can write a build that uses Kubernetes-native
+ resources to obtain your source code from a repository,
+ build it into container a image, and then run that image.`) }}
+ </p>
+
+ <template v-if="knativeInstalled">
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ readonly
+ />
+ </div>
+ </template>
+ <template v-else>
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ />
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 24a49624583..15cf4a56138 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -1,3 +1,10 @@
+// These need to match the enum found in app/models/clusters/cluster.rb
+export const CLUSTER_TYPE = {
+ INSTANCE: 'instance_type',
+ GROUP: 'group_type',
+ PROJECT: 'project_type',
+};
+
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable',
@@ -16,3 +23,4 @@ export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
+export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index a7d82292ba9..da562b09ee5 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -9,6 +9,7 @@ export default class ClusterService {
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
+ knative: this.options.installKnativeEndpoint,
};
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 106ac3cb516..e45da967392 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE } from '../constants';
export default class ClusterStore {
constructor() {
@@ -46,6 +46,14 @@ export default class ClusterStore {
requestReason: null,
hostname: null,
},
+ knative: {
+ title: s__('ClusterIntegration|Knative'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -93,6 +101,9 @@ export default class ClusterStore {
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
+ } else if (appId === KNATIVE) {
+ this.state.applications.knative.hostname =
+ serverAppEntry.hostname || this.state.applications.knative.hostname;
}
});
}
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
deleted file mode 100644
index 6c18a0fd390..00000000000
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import Vue from 'vue';
-import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
-
-Vue.component('gl-loading-icon', GlLoadingIcon);
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index ea945cd3fa5..0d2fe2925d8 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,5 +3,4 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
-import './gitlab_ui';
import '../lib/utils/axios_utils';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 539d0d29e0d..bffc025ced3 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
+import 'core-js/fn/object/values';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 10548da8ec5..ea74fd27ff6 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,7 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
deployKey: {
type: Object,
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 3589599986d..631a9673b3e 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -6,11 +6,13 @@ import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
KeysPanel,
NavigationTabs,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 59680959bb1..7c60fb3da42 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -21,6 +22,7 @@ export default {
HiddenFilesWarning,
CommitWidget,
TreeList,
+ GlLoadingIcon,
},
props: {
endpoint: {
@@ -223,7 +225,10 @@ export default {
:commit="commit"
/>
- <div class="files d-flex prepend-top-default">
+ <div
+ :data-can-create-note="getNoteableData.current_user.can_create_note"
+ class="files d-flex prepend-top-default"
+ >
<div
v-show="showTreeList"
class="diff-tree-list"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 29b5aff0fb1..a5b87dfc2d9 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -36,7 +36,7 @@ export default {
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
@@ -113,8 +113,8 @@ export default {
class="inline-parallel-buttons d-none d-md-flex ml-auto"
>
<a
- v-if="areAllFilesCollapsed"
- class="btn btn-default"
+ v-show="hasCollapsedFile"
+ class="btn btn-default append-right-8"
@click="expandAllFiles"
>
{{ __('Expand all') }}
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index fb5556e3cd7..547742a5ff4 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,15 +1,22 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import { diffModes } from '~/ide/constants';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
+import NoteForm from '../../notes/components/note_form.vue';
+import ImageDiffOverlay from './image_diff_overlay.vue';
+import DiffDiscussions from './diff_discussions.vue';
+import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
+import { getDiffMode } from '../store/utils';
export default {
components: {
InlineDiffView,
ParallelDiffView,
DiffViewer,
+ NoteForm,
+ DiffDiscussions,
+ ImageDiffOverlay,
},
props: {
diffFile: {
@@ -23,13 +30,38 @@ export default {
endpoint: state => state.diffs.endpoint,
}),
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ ...mapGetters('diffs', ['getCommentFormForDiffFile']),
+ ...mapGetters(['getNoteableData', 'noteableType']),
diffMode() {
- const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
- return diffModes[diffModeKey] || diffModes.replaced;
+ return getDiffMode(this.diffFile);
},
isTextFile() {
return this.diffFile.viewer.name === 'text';
},
+ diffFileCommentForm() {
+ return this.getCommentFormForDiffFile(this.diffFile.fileHash);
+ },
+ showNotesContainer() {
+ return this.diffFile.discussions.length || this.diffFileCommentForm;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
+ handleSaveNote(note) {
+ this.saveDiffDiscussion({
+ note,
+ formData: {
+ noteableData: this.getNoteableData,
+ noteableType: this.noteableType,
+ diffFile: this.diffFile,
+ positionType: IMAGE_DIFF_POSITION_TYPE,
+ x: this.diffFileCommentForm.x,
+ y: this.diffFileCommentForm.y,
+ width: this.diffFileCommentForm.width,
+ height: this.diffFileCommentForm.height,
+ },
+ });
+ },
},
};
</script>
@@ -56,7 +88,37 @@ export default {
:new-sha="diffFile.diffRefs.headSha"
:old-path="diffFile.oldPath"
:old-sha="diffFile.diffRefs.baseSha"
- :project-path="projectPath"/>
+ :file-hash="diffFile.fileHash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="diffFile.discussions"
+ :file-hash="diffFile.fileHash"
+ :can-comment="getNoteableData.current_user.can_create_note"
+ />
+ <div
+ v-if="showNotesContainer"
+ class="note-container"
+ >
+ <diff-discussions
+ v-if="diffFile.discussions.length"
+ class="diff-file-discussions"
+ :discussions="diffFile.discussions"
+ :should-collapse-discussions="true"
+ :render-avatar-badge="true"
+ />
+ <note-form
+ v-if="diffFileCommentForm"
+ ref="noteForm"
+ :is-editing="false"
+ :save-button-title="__('Comment')"
+ class="diff-comment-form new-note discussion-form discussion-form-container"
+ @handleFormUpdate="handleSaveNote"
+ @cancelForm="closeDiffFileCommentForm(diffFile.fileHash)"
+ />
+ </div>
+ </diff-viewer>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index cddbe554fbd..b9de487a737 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -1,24 +1,40 @@
<script>
import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
+ Icon,
},
props: {
discussions: {
type: Array,
required: true,
},
+ shouldCollapseDiscussions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ renderAvatarBadge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
methods: {
+ ...mapActions(['toggleDiscussion']),
...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion);
}
},
+ isExpanded(discussion) {
+ return this.shouldCollapseDiscussions ? discussion.expanded : true;
+ },
},
};
</script>
@@ -26,22 +42,53 @@ export default {
<template>
<div>
<div
- v-for="discussion in discussions"
+ v-for="(discussion, index) in discussions"
:key="discussion.id"
- class="discussion-notes diff-discussions"
+ :class="{
+ collapsed: !isExpanded(discussion)
+ }"
+ class="discussion-notes diff-discussions position-relative"
>
<ul
:data-discussion-id="discussion.id"
class="notes"
>
+ <template v-if="shouldCollapseDiscussions">
+ <button
+ :class="{
+ 'diff-notes-collapse': discussion.expanded,
+ 'btn-transparent badge badge-pill': !discussion.expanded
+ }"
+ type="button"
+ class="js-diff-notes-toggle"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="discussion.expanded"
+ name="collapse"
+ class="collapse-icon"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ </template>
<noteable-discussion
+ v-show="isExpanded(discussion)"
:discussion="discussion"
- :render-header="false"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler"
- />
+ >
+ <span
+ v-if="renderAvatarBadge"
+ slot="avatar-badge"
+ class="badge badge-pill"
+ >
+ {{ index + 1 }}
+ </span>
+ </noteable-discussion>
</ul>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 958e57c5652..e76c7afd863 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -10,6 +11,7 @@ export default {
components: {
DiffFileHeader,
DiffContent,
+ GlLoadingIcon,
},
props: {
file: {
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
new file mode 100644
index 00000000000..ae1b0a52901
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -0,0 +1,139 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'ImageDiffOverlay',
+ components: {
+ Icon,
+ },
+ props: {
+ discussions: {
+ type: [Array, Object],
+ required: true,
+ },
+ fileHash: {
+ type: String,
+ required: true,
+ },
+ canComment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showCommentIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ badgeClass: {
+ type: String,
+ required: false,
+ default: 'badge badge-pill',
+ },
+ shouldToggleDiscussion: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ ...mapGetters('diffs', ['getDiffFileByHash', 'getCommentFormForDiffFile']),
+ currentCommentForm() {
+ return this.getCommentFormForDiffFile(this.fileHash);
+ },
+ allDiscussions() {
+ return _.isArray(this.discussions) ? this.discussions : [this.discussions];
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ ...mapActions('diffs', ['openDiffFileCommentForm']),
+ getImageDimensions() {
+ return {
+ width: this.$parent.width,
+ height: this.$parent.height,
+ };
+ },
+ getPositionForObject(meta) {
+ const { x, y, width, height } = meta;
+ const imageWidth = this.getImageDimensions().width;
+ const imageHeight = this.getImageDimensions().height;
+ const widthRatio = imageWidth / width;
+ const heightRatio = imageHeight / height;
+
+ return {
+ x: Math.round(x * widthRatio),
+ y: Math.round(y * heightRatio),
+ };
+ },
+ getPosition(discussion) {
+ const { x, y } = this.getPositionForObject(discussion.position);
+
+ return {
+ left: `${x}px`,
+ top: `${y}px`,
+ };
+ },
+ clickedImage(x, y) {
+ const { width, height } = this.getImageDimensions();
+
+ this.openDiffFileCommentForm({
+ fileHash: this.fileHash,
+ width,
+ height,
+ x,
+ y,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="position-absolute w-100 h-100 image-diff-overlay">
+ <button
+ v-if="canComment"
+ type="button"
+ class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ @click="clickedImage($event.offsetX, $event.offsetY)"
+ >
+ <span class="sr-only">
+ {{ __('Add image comment') }}
+ </span>
+ </button>
+ <button
+ v-for="(discussion, index) in allDiscussions"
+ :key="discussion.id"
+ :style="getPosition(discussion)"
+ :class="badgeClass"
+ :disabled="!shouldToggleDiscussion"
+ class="js-image-badge"
+ type="button"
+ @click="toggleDiscussion({ discussionId: discussion.id })"
+ >
+ <icon
+ v-if="showCommentIcon"
+ name="image-comment-dark"
+ />
+ <template v-else>
+ {{ index + 1 }}
+ </template>
+ </button>
+ <button
+ v-if="currentCommentForm"
+ :style="{
+ left: `${currentCommentForm.x}px`,
+ top: `${currentCommentForm.y}px`
+ }"
+ :aria-label="__('Comment form position')"
+ class="btn-transparent comment-indicator"
+ type="button"
+ >
+ <icon
+ name="image-comment-dark"
+ />
+ </button>
+ </div>
+</template>
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 3339c56cbb6..3b71c0a1fd4 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -76,8 +76,9 @@ export default {
:class="className"
class="notes_holder"
>
- <td class="notes_line old"></td>
- <td class="notes_content parallel old">
+ <td
+ class="notes_content parallel old"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnLeft"
class="content"
@@ -95,8 +96,9 @@ export default {
line-position="left"
/>
</td>
- <td class="notes_line new"></td>
- <td class="notes_content parallel new">
+ <td
+ class="notes_content parallel new"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnRight"
class="content"
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 6a50d2c1426..78a39baa4cb 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -12,6 +12,7 @@ export const NOTE_TYPE = 'Note';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
export const TEXT_DIFF_POSITION_TYPE = 'text';
+export const IMAGE_DIFF_POSITION_TYPE = 'image';
export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ca8ae605cb4..d3e9c7c88f0 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -50,8 +50,8 @@ export const assignDiscussionsToDiff = (
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { fileHash, line_code } = removeDiscussion;
- commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
+ const { fileHash, line_code, id } = removeDiscussion;
+ commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code, id });
};
export const startRenderDiffsQueue = ({ state, commit }) => {
@@ -189,6 +189,7 @@ 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('closeDiffFileCommentForm', formData.diffFile.fileHash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
@@ -210,5 +211,19 @@ export const toggleShowTreeList = ({ commit, state }) => {
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
};
+export const openDiffFileCommentForm = ({ commit, getters }, formData) => {
+ const form = getters.getCommentFormForDiffFile(formData.fileHash);
+
+ if (form) {
+ commit(types.UPDATE_DIFF_FILE_COMMENT_FORM, formData);
+ } else {
+ commit(types.OPEN_DIFF_FILE_COMMENT_FORM, formData);
+ }
+};
+
+export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
+ commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index d4c205882ff..bf490f9d78a 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -5,7 +5,7 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
-export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
@@ -114,5 +114,8 @@ export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.
export const diffFilesLength = state => state.diffFiles.length;
+export const getCommentFormForDiffFile = state => fileHash =>
+ state.commentForms.find(form => form.fileHash === fileHash);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 1c5c35071de..085e255f1d3 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -24,4 +24,6 @@ export default () => ({
showTreeList:
storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true',
currentDiffFileId: '',
+ projectPath: '',
+ commentForms: [],
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6474ee628e2..e011031e72c 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -14,3 +14,7 @@ export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FIL
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
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';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 38a65f111a2..a7eea2c1449 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -153,20 +153,22 @@ export default {
});
},
- [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
+ [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode, id }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
if (selectedFile) {
- const targetLine = selectedFile.parallelDiffLines.find(
- line =>
- (line.left && line.left.lineCode === lineCode) ||
- (line.right && line.right.lineCode === lineCode),
- );
- if (targetLine) {
- const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
-
- Object.assign(targetLine[side], {
- discussions: [],
- });
+ if (selectedFile.parallelDiffLines) {
+ const targetLine = selectedFile.parallelDiffLines.find(
+ line =>
+ (line.left && line.left.lineCode === lineCode) ||
+ (line.right && line.right.lineCode === lineCode),
+ );
+ if (targetLine) {
+ const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
+
+ Object.assign(targetLine[side], {
+ discussions: [],
+ });
+ }
}
if (selectedFile.highlightedDiffLines) {
@@ -180,6 +182,12 @@ export default {
});
}
}
+
+ if (selectedFile.discussions && selectedFile.discussions.length) {
+ selectedFile.discussions = selectedFile.discussions.filter(
+ discussion => discussion.id !== id,
+ );
+ }
}
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
@@ -191,4 +199,25 @@ export default {
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
},
+ [types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) {
+ state.commentForms.push({
+ ...formData,
+ });
+ },
+ [types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) {
+ const { fileHash } = formData;
+
+ state.commentForms = state.commentForms.map(form => {
+ if (form.fileHash === fileHash) {
+ return {
+ ...formData,
+ };
+ }
+
+ return form;
+ });
+ },
+ [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
+ state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index a482a2b82c0..a935b9b1ffa 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { diffModes } from '~/ide/constants';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -34,6 +35,7 @@ export function getFormData(params) {
noteTargetLine,
diffViewType,
linePosition,
+ positionType,
} = params;
const position = JSON.stringify({
@@ -42,9 +44,13 @@ export function getFormData(params) {
head_sha: diffFile.diffRefs.headSha,
old_path: diffFile.oldPath,
new_path: diffFile.newPath,
- position_type: TEXT_DIFF_POSITION_TYPE,
- old_line: noteTargetLine.oldLine,
- new_line: noteTargetLine.newLine,
+ position_type: positionType || TEXT_DIFF_POSITION_TYPE,
+ old_line: noteTargetLine ? noteTargetLine.oldLine : null,
+ new_line: noteTargetLine ? noteTargetLine.newLine : null,
+ x: params.x,
+ y: params.y,
+ width: params.width,
+ height: params.height,
});
const postData = {
@@ -66,7 +72,7 @@ export function getFormData(params) {
diffFile.diffRefs.startSha && diffFile.diffRefs.headSha
? DIFF_NOTE_TYPE
: LEGACY_DIFF_NOTE_TYPE,
- line_code: noteTargetLine.lineCode,
+ line_code: noteTargetLine ? noteTargetLine.lineCode : null,
},
};
@@ -225,6 +231,7 @@ export function prepareDiffData(diffData) {
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ discussions: [],
});
}
}
@@ -320,3 +327,8 @@ export const generateTreeList = files =>
},
{ treeEntries: {}, tree: [] },
);
+
+export const getDiffMode = diffFile => {
+ const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}File`]);
+ return diffModes[diffModeKey] || diffModes.replaced;
+};
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 00d197d294f..a48f5fcb7d6 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
@@ -6,6 +7,7 @@ export default {
components: {
environmentTable,
tablePagination,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 0a3ae384afa..03c3ad0401f 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -4,6 +4,7 @@ import { formatTime } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -11,6 +12,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
actions: {
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index 7446196de13..1e8a892c0b8 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
-import { s__ } from '../../locale';
+import { s__ } from '~/locale';
/**
* Renders the external url link in environments table.
@@ -11,7 +11,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
externalUrl: {
@@ -28,12 +28,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="externalUrl"
class="btn external-url"
- data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 41f59447905..50b0e9747ee 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,7 +1,7 @@
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -36,7 +36,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -455,7 +455,7 @@ export default {
class="gl-responsive-table-row"
role="row">
<div
- v-tooltip
+ v-gl-tooltip
:title="model.name"
class="table-section section-wrap section-15 text-truncate"
role="gridcell"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 26bec125445..7c723fa8979 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,9 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { GlButton } from '@gitlab-org/gitlab-ui';
+import { GlButton, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
@@ -12,7 +11,7 @@ export default {
GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
monitoringUrl: {
@@ -29,12 +28,11 @@ export default {
</script>
<template>
<gl-button
- v-tooltip
+ v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="monitoring-url d-none d-sm-none d-md-block"
- data-container="body"
rel="noopener noreferrer nofollow"
variant="default"
>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 9e137f79dcc..298469e6482 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -6,19 +6,18 @@
* Makes a post request when the button is clicked.
*/
import { s__ } from '~/locale';
+import { GlTooltipDirective, GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
retryUrl: {
type: String,
@@ -55,21 +54,21 @@ export default {
</script>
<template>
<button
- v-tooltip
+ v-gl-tooltip
:disabled="isLoading"
:title="title"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
>
-
<icon
v-if="isLastDeployment"
- name="repeat" />
+ name="repeat"
+ />
<icon
v-else
- name="redo"/>
-
+ name="redo"
+ />
<gl-loading-icon v-if="isLoading" />
</button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index a814b3405f5..327c96a93e9 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,49 +5,42 @@
*/
import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
LoadingButton,
},
-
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
-
props: {
environment: {
type: Object,
required: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
computed: {
title() {
return s__('Environments|Stop environment');
},
},
-
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
-
beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
-
methods: {
onClick() {
$(this.$el).tooltip('dispose');
@@ -63,12 +56,11 @@ export default {
</script>
<template>
<loading-button
- v-tooltip
+ v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
container-class="btn btn-danger d-none d-sm-none d-md-block"
- data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 350417e5ad0..b8b909f350c 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,15 +3,15 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
terminalPath: {
@@ -29,12 +29,11 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:title="title"
:aria-label="title"
:href="terminalPath"
class="btn terminal-button d-none d-sm-none d-md-block"
- data-container="body"
>
<icon name="terminal" />
</a>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 16abafebbc0..c03d4f29ff9 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,11 +2,13 @@
/**
* Render environments table.
*/
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import environmentItem from './environment_item.vue';
export default {
components: {
environmentItem,
+ GlLoadingIcon,
},
props: {
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 657cc8cd1aa..6397f6caf1b 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
@@ -15,7 +15,7 @@ export default {
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
@@ -67,7 +67,7 @@ export default {
>
Stopping
<span
- v-tooltip
+ v-gl-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 70a8838b772..159c0bdc992 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
@@ -14,6 +15,7 @@ export default {
components: {
FrequentItemsSearchInput,
FrequentItemsList,
+ GlLoadingIcon,
},
mixins: [frequentItemsMixin],
props: {
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index a032f291546..2a4a39436e7 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -8,6 +8,7 @@ import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../event_hub';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
import groupsComponent from './groups.vue';
@@ -16,6 +17,7 @@ export default {
components: {
DeprecatedModal,
groupsComponent,
+ GlLoadingIcon,
},
props: {
action: {
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 52ccc537c9d..358f1153de2 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -2,12 +2,14 @@
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
export default {
components: {
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index a20dc0a7006..2d9bd99e82a 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,7 +1,11 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
message: {
type: Object,
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 94222c08e91..891f7d48b4c 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -2,10 +2,12 @@
import $ from 'jquery';
import { mapActions, mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
DropdownButton,
+ GlLoadingIcon,
},
props: {
data: {
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index acd37605d16..57da8b4e2cb 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -1,10 +1,12 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Stage from './stage.vue';
export default {
components: {
Stage,
+ GlLoadingIcon,
},
props: {
stages: {
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index ec168d36b9e..5644759d2f9 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -12,6 +13,7 @@ export default {
Icon,
CiIcon,
Item,
+ GlLoadingIcon,
},
props: {
stage: {
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index f5e42e87f1b..e4000f588bd 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -3,6 +3,7 @@ import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
@@ -16,6 +17,7 @@ export default {
TokenedInput,
Item,
Icon,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index b670b0355b7..16aec1decd6 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
@@ -17,6 +18,7 @@ export default {
Tab,
JobsList,
EmptyState,
+ GlLoadingIcon,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 37a8ad36507..0bd56ff6e9b 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils';
@@ -10,6 +11,7 @@ import { createPathWithExt } from '../../utils';
export default {
components: {
Navigator,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 42f23801692..af8959186f9 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -1,10 +1,12 @@
<script>
import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
components: {
Icon,
+ GlLoadingIcon,
},
props: {
manager: {
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index d23915966de..90216b04e92 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -16,6 +16,8 @@ import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue';
import Sidebar from './sidebar.vue';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '../mixins/delayed_job_mixin';
export default {
name: 'JobPageApp',
@@ -26,13 +28,14 @@ export default {
EmptyState,
EnvironmentsBlock,
ErasedBlock,
- GlLoadingIcon,
Icon,
Log,
LogTopBar,
StuckBlock,
Sidebar,
+ GlLoadingIcon,
},
+ mixins: [delayedJobMixin],
props: {
runnerSettingsUrl: {
type: String,
@@ -92,6 +95,17 @@ export default {
shouldRenderContent() {
return !this.isLoading && !this.hasError;
},
+
+ emptyStateTitle() {
+ const { emptyStateIllustration, remainingTime } = this;
+ const { title } = emptyStateIllustration;
+
+ if (this.isDelayedJob) {
+ return sprintf(title, { remainingTime });
+ }
+
+ return title;
+ },
},
watch: {
// Once the job log is loaded,
@@ -226,16 +240,16 @@ export default {
:erased-at="job.erased_at"
/>
- <div
+ <div
v-if="job.archived"
ref="sticky"
class="js-archived-job prepend-top-default archived-sticky sticky-top"
>
- <icon
+ <icon
name="lock"
class="align-text-bottom"
/>
-
+
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!--job log -->
@@ -272,7 +286,7 @@ export default {
class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
- :title="emptyStateIllustration.title"
+ :title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index cdac8a391d1..3ddcfd11dca 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,7 +1,10 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab-org/gitlab-ui';
+import tooltip from '~/vue_shared/directives/tooltip';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { sprintf } from '~/locale';
export default {
components: {
@@ -10,8 +13,9 @@ export default {
GlLink,
},
directives: {
- GlTooltip: GlTooltipDirective,
+ tooltip,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -24,7 +28,14 @@ export default {
},
computed: {
tooltipText() {
- return `${this.job.name} - ${this.job.status.tooltip}`;
+ const { name, status } = this.job;
+ const text = `${name} - ${status.tooltip}`;
+
+ if (this.isDelayedJob) {
+ return sprintf(text, { remainingTime: this.remainingTime });
+ }
+
+ return text;
},
},
};
@@ -39,7 +50,7 @@ export default {
}"
>
<gl-link
- v-gl-tooltip
+ v-tooltip
:href="job.status.details_path"
:title="tooltipText"
data-boundary="viewport"
diff --git a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
new file mode 100644
index 00000000000..8c7fb785a61
--- /dev/null
+++ b/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
@@ -0,0 +1,50 @@
+import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ remainingTime: formatTime(0),
+ remainingTimeIntervalId: null,
+ };
+ },
+
+ mounted() {
+ this.startRemainingTimeInterval();
+ },
+
+ beforeDestroy() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+ },
+
+ computed: {
+ isDelayedJob() {
+ return this.job && this.job.scheduled;
+ },
+ },
+
+ watch: {
+ isDelayedJob() {
+ this.startRemainingTimeInterval();
+ },
+ },
+
+ methods: {
+ startRemainingTimeInterval() {
+ if (this.remainingTimeIntervalId) {
+ clearInterval(this.remainingTimeIntervalId);
+ }
+
+ if (this.isDelayedJob) {
+ this.updateRemainingTime();
+ this.remainingTimeIntervalId = setInterval(() => this.updateRemainingTime(), 1000);
+ }
+ },
+
+ updateRemainingTime() {
+ const remainingMilliseconds = calculateRemainingMilliseconds(this.job.scheduled_at);
+ this.remainingTime = formatTime(remainingMilliseconds);
+ },
+ },
+};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 46740308f17..59007d5950e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -14,7 +14,7 @@ window.timeago = timeago;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = abbreviated => {
+export const getMonthNames = abbreviated => {
if (abbreviated) {
return [
s__('Jan'),
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
-export const stringifyTime = timeObject => {
+export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
+
+ if (fullNameFormat && isNonZero) {
+ // Remove traling 's' if unit value is singular
+ const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formatedUnitName}`;
+ }
+
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2950c2299ab..d8255181574 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -11,7 +11,6 @@ import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
-import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
@@ -207,8 +206,6 @@ export default class MergeRequestTabs {
}
this.resetViewContainer();
this.destroyPipelinesView();
-
- initDiscussionTab();
}
if (this.setUrl) {
this.setCurrentAction(action);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 554db102027..754c6e79ee4 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -321,10 +321,10 @@ Please check your network connection and try again.`;
v-else-if="!canCreateNote"
:issuable-type="issuableTypeTitle"
/>
- <ul
+ <div
v-else-if="canCreateNote"
class="notes notes-form timeline">
- <li class="timeline-entry">
+ <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">
@@ -462,7 +462,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</form>
</div>
</div>
- </li>
- </ul>
+ </div>
+ </div>
</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 eaa0cded224..b209f736c3f 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,15 +1,18 @@
<script>
import { mapState, mapActions } from 'vuex';
-import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
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-org/gitlab-ui';
-import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
GlSkeletonLoading,
+ DiffViewer,
+ ImageDiffOverlay,
},
props: {
discussion: {
@@ -25,7 +28,11 @@ export default {
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
+ projectPath: state => state.diffs.projectPath,
}),
+ diffMode() {
+ return getDiffMode(this.diffFile);
+ },
hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
},
@@ -62,11 +69,7 @@ export default {
},
},
mounted() {
- if (this.isImageDiff) {
- const canCreateNote = false;
- const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
- } else if (!this.hasTruncatedDiffLines) {
+ if (!this.hasTruncatedDiffLines) {
this.fetchDiff();
}
},
@@ -160,7 +163,24 @@ export default {
<div
v-else
>
- <div v-html="imageDiffHtml"></div>
+ <diff-viewer
+ :diff-mode="diffMode"
+ :new-path="diffFile.newPath"
+ :new-sha="diffFile.diffRefs.headSha"
+ :old-path="diffFile.oldPath"
+ :old-sha="diffFile.diffRefs.baseSha"
+ :file-hash="diffFile.fileHash"
+ :project-path="projectPath"
+ >
+ <image-diff-overlay
+ slot="image-overlay"
+ :discussions="discussion"
+ :file-hash="diffFile.fileHash"
+ :show-comment-icon="true"
+ :should-toggle-discussion="false"
+ badge-class="image-comment-badge"
+ />
+ </diff-viewer>
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 1f80f24e045..a4d76a70696 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,9 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedSvg from 'icons/_icon_status_success_solid.svg';
-import mrIssueSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionSvg from 'icons/_next_discussion.svg';
+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';
@@ -12,6 +9,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ Icon,
+ },
mixins: [discussionNavigation],
computed: {
...mapGetters([
@@ -37,12 +37,6 @@ export default {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
},
- created() {
- this.resolveSvg = resolveSvg;
- this.resolvedSvg = resolvedSvg;
- this.mrIssueSvg = mrIssueSvg;
- this.nextDiscussionSvg = nextDiscussionSvg;
- },
methods: {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
@@ -66,15 +60,9 @@ export default {
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
- type="button">
- <span
- v-if="allResolved"
- v-html="resolvedSvg"
- ></span>
- <span
- v-else
- v-html="resolveSvg"
- ></span>
+ type="button"
+ >
+ <icon name="check-circle" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
@@ -90,7 +78,7 @@ export default {
:title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
- <span v-html="mrIssueSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -103,7 +91,7 @@ export default {
data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion">
- <span v-html="nextDiscussionSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index e075f94b82b..f7a61fbfcd4 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,19 +1,14 @@
<script>
import { mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
-import editSvg from 'icons/_icon_pencil.svg';
-import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
-import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'NoteActions',
components: {
Icon,
+ GlLoadingIcon,
},
directives: {
tooltip,
@@ -108,15 +103,6 @@ export default {
return title;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- this.editSvg = editSvg;
- this.ellipsisSvg = ellipsisSvg;
- this.resolveDiscussionSvg = resolveDiscussionSvg;
- this.resolvedDiscussionSvg = resolvedDiscussionSvg;
- },
methods: {
onEdit() {
this.$emit('handleEdit');
@@ -150,12 +136,7 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve">
<template v-if="!isResolving">
- <div
- v-if="isResolved"
- v-html="resolvedDiscussionSvg"></div>
- <div
- v-else
- v-html="resolveDiscussionSvg"></div>
+ <icon name="check-circle" />
</template>
<gl-loading-icon
v-else
@@ -177,18 +158,18 @@ export default {
title="Add reaction"
>
<gl-loading-icon inline/>
- <span
- class="link-highlight award-control-icon-neutral"
- v-html="emojiSmiling">
- </span>
- <span
- class="link-highlight award-control-icon-positive"
- v-html="emojiSmiley">
- </span>
- <span
- class="link-highlight award-control-icon-super-positive"
- v-html="emojiSmile">
- </span>
+ <icon
+ css-classes="link-highlight award-control-icon-neutral"
+ name="emoji_slightly_smiling_face"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-positive"
+ name="emoji_smiley"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-super-positive"
+ name="emoji_smiley"
+ />
</a>
</div>
<div
@@ -202,10 +183,10 @@ export default {
data-container="body"
data-placement="bottom"
@click="onEdit">
- <span
- class="link-highlight"
- v-html="editSvg">
- </span>
+ <icon
+ name="pencil"
+ css-classes="link-highlight"
+ />
</button>
</div>
<div
@@ -238,15 +219,15 @@ export default {
data-toggle="dropdown"
data-container="body"
data-placement="bottom">
- <span
- class="icon"
- v-html="ellipsisSvg">
- </span>
+ <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 as abuse
+ {{ __('Report abuse to GitLab') }}
</a>
</li>
<li v-if="noteUrl">
@@ -255,7 +236,7 @@ export default {
type="button"
class="btn-default btn-transparent js-btn-copy-note-link"
>
- Copy link
+ {{ __('Copy link') }}
</button>
</li>
<li v-if="canEdit">
@@ -264,7 +245,7 @@ export default {
type="button"
@click.prevent="onDelete">
<span class="text-danger">
- Delete comment
+ {{ __('Delete comment') }}
</span>
</button>
</li>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index df7ab4502a6..401bcfabbe4 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,13 +1,14 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
+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,
},
@@ -72,11 +73,6 @@ export default {
return this.noteAuthorId === this.getUserData.id;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- },
methods: {
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
@@ -196,17 +192,14 @@ export default {
data-boundary="viewport"
data-placement="bottom"
type="button">
- <span
- class="award-control-icon award-control-icon-neutral"
- v-html="emojiSmiling">
+ <span class="award-control-icon award-control-icon-neutral">
+ <icon name="emoji_slightly_smiling_face" />
</span>
- <span
- class="award-control-icon award-control-icon-positive"
- v-html="emojiSmiley">
+ <span class="award-control-icon award-control-icon-positive">
+ <icon name="emoji_smiley" />
</span>
- <span
- class="award-control-icon award-control-icon-super-positive"
- v-html="emojiSmile">
+ <span class="award-control-icon award-control-icon-super-positive">
+ <icon name="emoji_smiley" />
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 7b6e7b72caf..dd7313d7b10 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -45,6 +45,9 @@ export default {
noteTimestampLink() {
return `#note_${this.noteId}`;
},
+ hasAuthor() {
+ return this.author && Object.keys(this.author).length;
+ },
},
methods: {
...mapActions(['setTargetNoteHash']),
@@ -76,7 +79,7 @@ export default {
</button>
</div>
<a
- v-if="Object.keys(author).length"
+ v-if="hasAuthor"
:href="author.path"
>
<span class="note-header-author-name">{{ author.name }}</span>
@@ -92,9 +95,6 @@ export default {
</span>
<span class="note-headline-light">
<span class="note-headline-meta">
- <template v-if="actionText">
- {{ actionText }}
- </template>
<span class="system-note-message">
<slot></slot>
</span>
@@ -102,7 +102,9 @@ export default {
v-if="createdAt"
>
<span class="system-note-separator">
- &middot;
+ <template v-if="actionText">
+ {{ actionText }}
+ </template>
</span>
<a
:href="noteTimestampLink"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 6293dd5b7e1..a153edd0476 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import { s__ } from '~/locale';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
+import icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
+import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
@@ -26,6 +26,7 @@ import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
+ icon,
noteableNote,
diffWithNote,
userAvatarLink,
@@ -33,6 +34,7 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ toggleRepliesWidget,
placeholderNote,
placeholderSystemNote,
systemNote,
@@ -46,11 +48,6 @@ export default {
type: Object,
required: true,
},
- renderHeader: {
- type: Boolean,
- required: false,
- default: true,
- },
renderDiffFile: {
type: Boolean,
required: false,
@@ -72,6 +69,7 @@ export default {
isReplying: false,
isResolving: false,
resolveAsThread: true,
+ isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false,
};
},
computed: {
@@ -112,6 +110,15 @@ export default {
newNotePath() {
return this.getNoteableData.create_note_path;
},
+ hasReplies() {
+ return this.discussion.notes.length > 1;
+ },
+ initialDiscussion() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -147,6 +154,12 @@ export default {
return diffDiscussion && diffFile && this.renderDiffFile;
},
+ shouldGroupReplies() {
+ return !this.shouldRenderDiffs && !this.transformedDiscussion.diffDiscussion;
+ },
+ shouldRenderHeader() {
+ return this.shouldRenderDiffs;
+ },
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
},
@@ -160,6 +173,22 @@ export default {
wrapperClass() {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
+ componentClassName() {
+ if (this.shouldRenderDiffs) {
+ if (!this.lastUpdatedAt && !this.discussion.resolved) {
+ return 'unresolved';
+ }
+ }
+
+ return '';
+ },
+ shouldShowDiscussions() {
+ const isExpanded = this.discussion.expanded;
+ const { diffDiscussion, resolved } = this.transformedDiscussion;
+ const isResolvedNonDiffDiscussion = !diffDiscussion && resolved;
+
+ return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ },
},
watch: {
isReplying() {
@@ -173,10 +202,6 @@ export default {
}
},
},
- created() {
- this.resolveDiscussionsSvg = resolveDiscussionsSvg;
- this.nextDiscussionsSvg = nextDiscussionsSvg;
- },
methods: {
...mapActions([
'saveNote',
@@ -207,6 +232,9 @@ export default {
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
+ toggleReplies() {
+ this.isRepliesCollapsed = !this.isRepliesCollapsed;
+ },
showReplyForm() {
this.isReplying = true;
},
@@ -274,26 +302,29 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ class="note note-discussion timeline-entry"
+ :class="componentClassName"
+ >
<div class="timeline-entry-inner">
- <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>
<div class="timeline-content">
<div
:data-discussion-id="transformedDiscussion.discussion_id"
class="discussion js-discussion-container"
>
<div
- v-if="renderHeader"
- class="discussion-header"
+ 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"
@@ -339,7 +370,7 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="discussion.expanded || alwaysExpanded"
+ v-if="shouldShowDiscussions"
class="discussion-body">
<component
:is="wrapperComponent"
@@ -348,38 +379,70 @@ Please check your network connection and try again.`;
>
<div class="discussion-notes">
<ul class="notes">
- <component
- :is="componentName(note)"
- v-for="note in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
+ <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>
+ <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="btn-group d-flex discussion-with-resolve-btn"
- role="group">
- <div
- class="btn-group w-100"
- role="group">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm">Reply...</button>
- </div>
- <div
- v-if="discussion.resolvable"
- class="btn-group"
- role="group">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mx-sm-2"
@click="resolveHandler()"
>
<i
@@ -407,7 +470,7 @@ Please check your network connection and try again.`;
btn-default discussion-create-issue-btn"
data-container="body"
>
- <span v-html="resolveDiscussionsSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -421,7 +484,7 @@ Please check your network connection and try again.`;
data-container="body"
@click="jumpToNextDiscussion"
>
- <span v-html="nextDiscussionsSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index f391ed848a4..e302a89ee95 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -173,7 +173,7 @@ export default {
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry"
+ class="note timeline-entry note-wrapper"
>
<div class="timeline-entry-inner">
<div class="timeline-icon">
@@ -182,7 +182,13 @@ export default {
: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">
@@ -190,6 +196,7 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
+ action-text="commented"
/>
<note-actions
:author-id="author.id"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index ed5ac112dc0..e555279a6ac 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,6 +50,7 @@ export default {
},
data() {
return {
+ isFetching: false,
currentFilter: null,
};
},
@@ -141,6 +142,10 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
+ if (this.isFetching) return null;
+
+ this.isFetching = true;
+
return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
@@ -149,6 +154,7 @@ export default {
this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
+ this.isFetching = false;
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
new file mode 100644
index 00000000000..78ecbbb9247
--- /dev/null
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -0,0 +1,94 @@
+<script>
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TimeAgoTooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ replies: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ lastReply() {
+ return this.replies[this.replies.length - 1];
+ },
+ uniqueAuthors() {
+ const authors = this.replies.map(reply => reply.author || {});
+
+ return _.uniq(authors, author => author.username);
+ },
+ className() {
+ return this.collapsed ? 'collapsed' : 'expanded';
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="className"
+ class="replies-toggle"
+ >
+ <template v-if="collapsed">
+ <icon
+ name="chevron-right"
+ @click.native="toggle"
+ />
+ <div>
+ <user-avatar-link
+ v-for="author in uniqueAuthors"
+ :key="author.username"
+ :link-href="author.path"
+ :img-alt="author.name"
+ :img-src="author.avatar_url"
+ :img-size="26"
+ :tooltip-text="author.name"
+ tooltip-placement="bottom"
+ />
+ </div>
+ <button
+ class="btn btn-link js-replies-text"
+ type="button"
+ @click="toggle"
+ >
+ {{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
+ </button>
+ {{ __('Last reply by') }}
+ <a
+ :href="lastReply.author.path"
+ class="btn btn-link author-link"
+ >
+ {{ lastReply.author.name }}
+ </a>
+ <time-ago-tooltip
+ :time="lastReply.created_at"
+ tooltip-placement="bottom"
+ />
+ </template>
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies"
+ @click="toggle"
+ >
+ <icon name="chevron-down" />
+ {{ s__('Notes|Collapse replies') }}
+ </span>
+ </li>
+</template>
diff --git a/app/assets/javascripts/pages/groups/clusters/destroy/index.js b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
new file mode 100644
index 00000000000..845a5f7042c
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -0,0 +1,5 @@
+import initDismissableCallout from '~/dismissable_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initDismissableCallout('.gcp-signup-offer');
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/show/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/update/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/clusters/update/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
new file mode 100644
index 00000000000..bf80d8b8193
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -0,0 +1,16 @@
+import initDismissableCallout from '~/dismissable_callout';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const { page } = document.body.dataset;
+ const newClusterViews = [
+ 'groups:clusters:new',
+ 'groups:clusters:create_gcp',
+ 'groups:clusters:create_user',
+ ];
+
+ if (newClusterViews.indexOf(page) > -1) {
+ 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 e4b8baede58..845a5f7042c 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,5 @@
-import ClustersIndex from '~/clusters/clusters_index';
+import initDismissableCallout from '~/dismissable_callout';
document.addEventListener('DOMContentLoaded', () => {
- new ClustersIndex(); // eslint-disable-line no-new
+ initDismissableCallout('.gcp-signup-offer');
});
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 23c0be7742e..4de8b3401e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import StageColumnComponent from './stage_column_component.vue';
export default {
components: {
StageColumnComponent,
+ GlLoadingIcon,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index a1504592bbc..7cdde8a53b3 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -2,6 +2,8 @@
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { sprintf } from '~/locale';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -36,6 +38,7 @@ export default {
directives: {
tooltip,
},
+ mixins: [delayedJobMixin],
props: {
job: {
type: Object,
@@ -52,6 +55,7 @@ export default {
default: Infinity,
},
},
+
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -59,17 +63,23 @@ export default {
tooltipText() {
const textBuilder = [];
+ const { name: jobName } = this.job;
- if (this.job.name) {
- textBuilder.push(this.job.name);
+ if (jobName) {
+ textBuilder.push(jobName);
}
- if (this.job.name && this.status.tooltip) {
+ const { tooltip: statusTooltip } = this.status;
+ if (jobName && statusTooltip) {
textBuilder.push('-');
}
- if (this.status.tooltip) {
- textBuilder.push(this.job.status.tooltip);
+ if (statusTooltip) {
+ if (this.isDelayedJob) {
+ textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime }));
+ } else {
+ textBuilder.push(statusTooltip);
+ }
}
return textBuilder.join(' ');
@@ -88,6 +98,7 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+
methods: {
pipelineActionRequestComplete() {
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 1f9187c3d65..8f004b491c8 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
@@ -6,6 +7,7 @@ export default {
name: 'PipelineHeaderSection',
components: {
ciHeader,
+ GlLoadingIcon,
},
props: {
pipeline: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 07a4af3e61e..cb47704ca26 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -4,6 +4,7 @@ import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -12,6 +13,7 @@ export default {
components: {
Icon,
GlCountdown,
+ GlLoadingIcon,
},
props: {
actions: {
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 7ec55792850..3df8f7a6da6 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -13,6 +13,7 @@
*/
import $ from 'jquery';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
@@ -26,6 +27,7 @@ export default {
components: {
Icon,
JobItem,
+ GlLoadingIcon,
},
directives: {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 85781f548c6..41bc5dcce5c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,4 +1,5 @@
import Visibility from 'visibilityjs';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
@@ -13,6 +14,7 @@ export default {
PipelinesTableComponent,
SvgBlankState,
EmptyState,
+ GlLoadingIcon,
},
data() {
return {
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
index d5266544307..f5dae5ad808 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import store from '../store';
@@ -11,6 +12,7 @@ export default {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
+ GlLoadingIcon,
},
props: {
fieldId: {
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 120b4fc2f2b..9a729ca9b91 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -5,6 +5,7 @@ import Poll from '~/lib/utils/poll';
import Flash from '~/flash';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
@@ -13,6 +14,7 @@ export default {
},
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index 9dd1c87a87d..0a906f40f5a 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,5 +1,6 @@
<script>
import { mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
@@ -9,6 +10,7 @@ export default {
name: 'RegistryListApp',
components: {
collapsibleContainer,
+ GlLoadingIcon,
},
props: {
endpoint: {
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 501b2625ae5..be9816a55c4 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +13,7 @@ export default {
components: {
clipboardButton,
tableRegistry,
+ GlLoadingIcon,
},
directives: {
tooltip,
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 51188981bed..a44ba833b63 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -1,6 +1,7 @@
<script>
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/**
* Renders the summary row for each report
@@ -15,6 +16,7 @@ export default {
components: {
CiIcon,
Popover,
+ GlLoadingIcon,
},
props: {
summary: {
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 11b5dbe5f8e..fe73f6a0cef 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -2,6 +2,7 @@
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
directives: {
@@ -9,6 +10,7 @@ export default {
},
components: {
userAvatarImage,
+ GlLoadingIcon,
},
props: {
loading: {
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index bc59774f0a8..913a616d9f1 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,6 +1,7 @@
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
@@ -13,6 +14,7 @@ export default {
},
components: {
Icon,
+ GlLoadingIcon,
},
props: {
issuableId: {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 4b090212d83..ce051582299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -696,17 +696,21 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return (
- "<div class='user-result " +
- (!user.username ? 'no-username' : void 0) +
- "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" +
- avatar +
- "'></div> <div class='user-name dropdown-menu-user-full-name'>" +
- _.escape(user.name) +
- "</div> <div class='user-username dropdown-menu-user-username'>" +
- (!user.invite ? '@' + _.escape(user.username) : '') +
- '</div> </div>'
- );
+ return `
+ <div class='user-result'>
+ <div class='user-image'>
+ <img class='avatar avatar-inline s32' src='${avatar}'>
+ </div>
+ <div class='user-info'>
+ <div class='user-name dropdown-menu-user-full-name'>
+ ${_.escape(user.name)}
+ </div>
+ <div class='user-username dropdown-menu-user-username text-secondary'>
+ ${!user.invite ? '@' + _.escape(user.username) : ''}
+ </div>
+ </div>
+ </div>
+ `;
};
UsersSelect.prototype.formatSelection = function(user) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ba6a1687e51..b3340290ed3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
+ GlLoadingIcon,
},
props: {
status: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 4f8b07484c0..4bfbdcf1404 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -6,6 +7,7 @@ export default {
name: 'MRWidgetAutoMergeFailed',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 656c3b5c47e..7e33021e4b4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -6,6 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
name: 'MRWidgetMerged',
@@ -16,6 +17,7 @@ export default {
MrWidgetAuthorTime,
statusIcon,
ClipboardButton,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 041fa13a8f5..0e714cc2aa1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -8,6 +9,7 @@ export default {
name: 'MRWidgetRebase',
components: {
statusIcon,
+ GlLoadingIcon,
},
props: {
mr: {
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 8684005e0fb..766fc211bf5 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -1,5 +1,5 @@
<script>
-import tooltip from '~/vue_shared/directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
@@ -10,7 +10,7 @@ export default {
Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
file: {
@@ -79,10 +79,8 @@ export default {
<template>
<span
- v-tooltip
+ v-gl-tooltip.right
:title="tooltipTitle"
- data-container="body"
- data-placement="right"
class="file-changed-icon ml-auto"
>
<icon
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index c60052fec50..6780254827f 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,6 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import CiIcon from './ci_icon.vue';
-import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
@@ -27,7 +27,7 @@ export default {
CiIcon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
status: {
@@ -50,7 +50,7 @@ export default {
</script>
<template>
<a
- v-tooltip
+ v-gl-tooltip
:href="status.details_path"
:class="cssClass"
:title="!showText ? status.text : ''"
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 151eee75d44..b1139f34e41 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,11 +1,11 @@
<script>
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
-import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
UserAvatarLink,
@@ -124,11 +124,10 @@ export default {
</div>
<a
- v-tooltip
+ v-gl-tooltip
:href="commitRef.ref_url"
:title="commitRef.name"
class="ref-name"
- data-container="body"
>
{{ commitRef.name }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 8163947cd0c..6f2f0f98690 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -17,19 +17,37 @@ export default {
type: Boolean,
default: true,
},
+ innerCssClasses: {
+ type: [Array, Object, String],
+ required: false,
+ default: '',
+ },
},
data() {
return {
width: 0,
height: 0,
- isZoomable: false,
- isZoomed: false,
+ isLoaded: false,
};
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
+ dimensionStyles() {
+ if (!this.isLoaded) return {};
+
+ return {
+ width: `${this.width}px`,
+ height: `${this.height}px`,
+ };
+ },
+ hasFileSize() {
+ return this.fileSize > 0;
+ },
+ hasDimensions() {
+ return this.width && this.height;
+ },
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@@ -48,51 +66,52 @@ export default {
const { contentImg } = this.$refs;
if (contentImg) {
- this.isZoomable =
- contentImg.naturalWidth > contentImg.width ||
- contentImg.naturalHeight > contentImg.height;
-
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
- this.$emit('imgLoaded', {
- width: this.width,
- height: this.height,
- renderedWidth: contentImg.clientWidth,
- renderedHeight: contentImg.clientHeight,
+ this.$nextTick(() => {
+ this.isLoaded = true;
+
+ this.$emit('imgLoaded', {
+ width: this.width,
+ height: this.height,
+ renderedWidth: contentImg.clientWidth,
+ renderedHeight: contentImg.clientHeight,
+ });
});
}
},
- onImgClick() {
- if (this.isZoomable) this.isZoomed = !this.isZoomed;
- },
},
};
</script>
<template>
- <div class="file-container">
- <div class="file-content image_file">
+ <div>
+ <div
+ :class="innerCssClasses"
+ :style="dimensionStyles"
+ class="position-relative"
+ >
<img
ref="contentImg"
- :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }"
:src="path"
- :alt="path"
@load="onImgLoad"
- @click="onImgClick"/>
- <p
- v-if="renderInfo"
- class="file-info prepend-top-10">
- <template v-if="fileSize>0">
- {{ fileSizeReadable }}
- </template>
- <template v-if="fileSize>0 && width && height">
- |
- </template>
- <template v-if="width && height">
- W: {{ width }} | H: {{ height }}
- </template>
- </p>
+ />
+ <slot name="image-overlay"></slot>
</div>
+ <p
+ v-if="renderInfo"
+ class="image-info"
+ >
+ <template v-if="hasFileSize">
+ {{ fileSizeReadable }}
+ </template>
+ <template v-if="hasFileSize && hasDimensions">
+ |
+ </template>
+ <template v-if="hasDimensions">
+ <strong>W</strong>: {{ width }} | <strong>H</strong>: {{ height }}
+ </template>
+ </p>
</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 cfc5343217c..9c3f3e7f7a9 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
@@ -69,6 +69,13 @@ export default {
:new-path="fullNewPath"
:old-path="fullOldPath"
:project-path="projectPath"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
+ <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
index 38e881d17a2..cd0c1e850af 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -15,11 +15,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -120,7 +115,6 @@ export default {
key="onionOldImg"
:render-info="false"
:path="oldPath"
- :project-path="projectPath"
@imgLoaded="onionOldImgLoaded"
/>
</div>
@@ -136,9 +130,14 @@ export default {
key="onionNewImg"
:render-info="false"
:path="newPath"
- :project-path="projectPath"
@imgLoaded="onionNewImgLoaded"
- />
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<div class="controls">
<div class="transparent"></div>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
index 86366c799a2..c3cfe54eb4d 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -16,11 +16,6 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
@@ -117,16 +112,14 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-frame">
- <div class="frame deleted">
- <image-viewer
- key="swipeOldImg"
- ref="swipeOldImg"
- :render-info="false"
- :path="oldPath"
- :project-path="projectPath"
- @imgLoaded="swipeOldImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeOldImg"
+ ref="swipeOldImg"
+ :render-info="false"
+ :path="oldPath"
+ class="frame deleted"
+ @imgLoaded="swipeOldImgLoaded"
+ />
<div
ref="swipeWrap"
:style="{
@@ -134,15 +127,19 @@ export default {
'height': swipeMaxPixelHeight,
}"
class="swipe-wrap">
- <div class="frame added">
- <image-viewer
- key="swipeNewImg"
- :render-info="false"
- :path="newPath"
- :project-path="projectPath"
- @imgLoaded="swipeNewImgLoaded"
- />
- </div>
+ <image-viewer
+ key="swipeNewImg"
+ :render-info="false"
+ :path="newPath"
+ class="frame added"
+ @imgLoaded="swipeNewImgLoaded"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
<span
ref="swipeBar"
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
index 9c19266ecdf..9806d65e940 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -14,28 +14,29 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
};
</script>
<template>
- <div class="two-up view row">
- <div class="col-sm-6 frame deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
- </div>
- <div class="col-sm-6 frame added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
- </div>
+ <div class="two-up view">
+ <image-viewer
+ :path="oldPath"
+ :render-info="true"
+ inner-css-classes="frame deleted"
+ class="wrap"
+ />
+ <image-viewer
+ :path="newPath"
+ :render-info="true"
+ :inner-css-classes="['frame', 'added']"
+ class="wrap"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index 1af85283277..e68a2aa73fa 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -8,9 +8,6 @@ import { diffModes, imageViewMode } from '../constants';
export default {
components: {
ImageViewer,
- TwoUpViewer,
- SwipeViewer,
- OnionSkinViewer,
},
props: {
diffMode: {
@@ -25,17 +22,32 @@ export default {
type: String,
required: true,
},
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
mode: imageViewMode.twoup,
};
},
+ computed: {
+ imageViewComponent() {
+ switch (this.mode) {
+ case imageViewMode.twoup:
+ return TwoUpViewer;
+ case imageViewMode.swipe:
+ return SwipeViewer;
+ case imageViewMode.onion:
+ return OnionSkinViewer;
+ default:
+ return undefined;
+ }
+ },
+ isNew() {
+ return this.diffMode === diffModes.new;
+ },
+ imagePath() {
+ return this.isNew ? this.newPath : this.oldPath;
+ },
+ },
methods: {
changeMode(newMode) {
this.mode = newMode;
@@ -52,15 +64,16 @@ export default {
v-if="diffMode === $options.diffModes.replaced"
class="diff-viewer">
<div class="image js-replaced-image">
- <two-up-viewer
- v-if="mode === $options.imageViewMode.twoup"
- v-bind="$props"/>
- <swipe-viewer
- v-else-if="mode === $options.imageViewMode.swipe"
- v-bind="$props"/>
- <onion-skin-viewer
- v-else-if="mode === $options.imageViewMode.onion"
- v-bind="$props"/>
+ <component
+ :is="imageViewComponent"
+ v-bind="$props"
+ >
+ <slot
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </component>
</div>
<div class="view-modes">
<ul class="view-modes-menu">
@@ -87,23 +100,27 @@ export default {
</li>
</ul>
</div>
- <div class="note-container"></div>
- </div>
- <div
- v-else-if="diffMode === $options.diffModes.new"
- class="diff-viewer added">
- <image-viewer
- :path="newPath"
- :project-path="projectPath"
- />
</div>
<div
v-else
- class="diff-viewer deleted">
- <image-viewer
- :path="oldPath"
- :project-path="projectPath"
- />
+ class="diff-viewer"
+ >
+ <div class="image">
+ <image-viewer
+ :path="imagePath"
+ :inner-css-classes="['frame', {
+ 'added': isNew,
+ 'deleted': diffMode === $options.diffModes.deleted
+ }]"
+ >
+ <slot
+ v-if="isNew"
+ slot="image-overlay"
+ name="image-overlay"
+ >
+ </slot>
+ </image-viewer>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 31087017968..0e194eaaed5 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -1,7 +1,11 @@
<script>
import { __ } from '~/locale';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
isDisabled: {
type: Boolean,
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 408f7d7965f..03818be6a69 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import getIconForFile from './file_icon/file_icon_map';
import icon from '../../vue_shared/components/icon.vue';
@@ -17,6 +18,7 @@ import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
icon,
+ GlLoadingIcon,
},
props: {
fileName: {
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index cddebfae115..a25841fc02f 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,6 +1,6 @@
<script>
// only allow classes in images.scss e.g. s12
-const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
/*
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index f9b7fd5b1f9..69d7e5c46f5 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
/* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
@@ -18,6 +19,9 @@
*/
export default {
+ components: {
+ GlLoadingIcon,
+ },
props: {
loading: {
type: Boolean,
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 f56414c3c63..8cb72afcdc0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <li class="timeline-entry note">
+ <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon">
</div>
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 de3c7a80365..6a44e6a29ed 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -76,7 +76,7 @@ export default {
<li
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry">
+ class="note system-note timeline-entry note-wrapper">
<div class="timeline-entry-inner">
<div
class="timeline-icon"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 500586302cf..5b12bb6b59e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import datePicker from '../pikaday.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
@@ -10,6 +11,7 @@ export default {
datePicker,
toggleSidebar,
collapsedCalendarIcon,
+ GlLoadingIcon,
},
props: {
blockClass: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 3df286de129..e50d612ce36 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
@@ -24,6 +25,7 @@ export default {
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
+ GlLoadingIcon,
},
props: {
showCreate: {
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 4e9289cbed8..e7cb5cfac12 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,4 +1,5 @@
<script>
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
import icon from './icon.vue';
@@ -10,6 +11,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
icon,
+ GlLoadingIcon,
},
model: {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 7737b9f2697..4cfb1ded0a9 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -15,14 +15,14 @@
*/
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
- directives: {
- tooltip,
+ components: {
+ GlTooltip,
},
props: {
lazy: {
@@ -73,9 +73,6 @@ export default {
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
- tooltipContainer() {
- return this.tooltipText ? 'body' : null;
- },
avatarSizeClass() {
return `s${this.size}`;
},
@@ -84,22 +81,30 @@ export default {
</script>
<template>
- <img
- v-tooltip
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- :title="tooltipText"
- class="avatar"
- data-boundary="window"
- />
+ <span>
+ <img
+ ref="userAvatarImage"
+ :class="{
+ lazy: lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ class="avatar"
+ />
+ <gl-tooltip
+ :target="() => $refs.userAvatarImage"
+ :placement="tooltipPlacement"
+ boundary="window"
+ class="js-user-avatar-image-toolip"
+ >
+ <slot>
+ {{ tooltipText }}
+ </slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 86c7498a092..351a639c6e8 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -17,9 +17,8 @@
*/
-import { GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -28,7 +27,7 @@ export default {
userAvatarImage,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
linkHref: {
@@ -94,11 +93,14 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
- /><span
+ >
+ <slot></slot>
+ </user-avatar-image><span
v-if="shouldShowUsername"
- v-tooltip
+ v-gl-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
- >{{ username }}</span>
+ class="js-user-avatar-link-username"
+ >{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link>
</template>
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 702276780e9..7a95db5976d 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -148,10 +148,7 @@
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
-
- path {
- fill: $award-emoji-positive-add-lines;
- }
+ fill: $award-emoji-positive-add-lines;
}
.award-control-icon-neutral {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c4296c7a88a..219fd99b097 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -207,6 +207,10 @@
@include btn-with-margin;
}
+ &.btn-icon {
+ color: $gl-gray-700;
+ }
+
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
@@ -218,6 +222,25 @@
}
}
+ &.btn-text-field {
+ width: 100%;
+ text-align: left;
+ padding: 6px 16px;
+ border-color: $border-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
+
+ &:hover,
+ &:active,
+ &:focus {
+ cursor: text;
+ box-shadow: none;
+ border-color: lighten($blue-300, 20%);
+ color: $gray-darkest;
+ background-color: $gray-light;
+ }
+ }
+
&.dot-highlight::after {
content: '';
background-color: $blue-500;
@@ -335,25 +358,6 @@
}
}
-.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $gray-light;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- background-color: $gray-light;
- }
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index fa753b13e5f..626c8f92d1d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -33,6 +33,11 @@
color: $brand-danger;
}
+.text-danger-muted,
+.text-danger-muted:hover {
+ color: $red-300;
+}
+
.text-warning,
.text-warning:hover {
color: $brand-warning;
@@ -345,6 +350,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
+.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
@@ -365,6 +371,7 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 53f198b47c6..6bdcb20210b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,7 +36,6 @@
text-align: left;
padding: 10px $gl-padding;
word-wrap: break-word;
- border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear {
padding-left: 0;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 381c0290d32..7f0edd88dfb 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -243,6 +243,8 @@
.user-result {
min-height: 24px;
+ display: flex;
+ align-items: center;
.user-image {
float: left;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index dfb145debe7..4a311da1675 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -1,7 +1,7 @@
.timeline {
- @include basic-list;
margin: 0;
padding: 0;
+ list-style: none;
&::before {
@include notes-media('max', map-get($grid-breakpoints, sm)) {
@@ -26,10 +26,8 @@
}
.timeline-entry {
- border-color: $white-normal;
color: $gl-text-color;
- border-bottom: 1px solid $border-white-light;
- background: $white-light;
+ background-color: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bfcac3f1c3f..f4540146a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -195,6 +195,7 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
+$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -440,7 +441,7 @@ $ci-skipped-color: #888;
* Boards
*/
$issue-boards-font-size: 14px;
-$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
@@ -640,3 +641,8 @@ Modals
$modal-body-height: 134px;
$priority-label-empty-state-width: 114px;
+
+/*
+Issues Analytics
+*/
+$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 54fbd40cece..c6074eb9df4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -90,20 +90,14 @@
}
.with-performance-bar & {
- height: calc(
- 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@include media-breakpoint-only(sm) {
- height: calc(
- 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
@include media-breakpoint-up(md) {
- height: calc(
- 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
@@ -271,7 +265,7 @@
height: 100%;
width: 100%;
margin-bottom: 0;
- padding: 5px;
+ padding: $gl-padding-4;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
@@ -284,14 +278,16 @@
.board-card {
position: relative;
- padding: 11px 10px 11px $gl-padding;
+ padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
+ line-height: $gl-padding;
&:not(:last-child) {
- margin-bottom: 5px;
+ margin-bottom: $gl-padding-8;
}
&.is-active,
@@ -302,113 +298,120 @@
.badge {
border: 0;
outline: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ @include media-breakpoint-down(lg) {
+ font-size: $gl-font-size-xs;
+ padding-left: $gl-padding-4;
+ padding-right: $gl-padding-4;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+
+ svg {
+ vertical-align: top;
}
.confidential-icon {
- vertical-align: text-top;
- margin-right: 5px;
+ color: $orange-600;
+ cursor: help;
+ }
+
+ @include media-breakpoint-down(md) {
+ padding: $gl-padding-8;
}
}
.board-card-title {
@include overflow-break-word();
- margin: 0 30px 0 0;
font-size: 1em;
- line-height: inherit;
a {
color: $gl-text-color;
- margin-right: 2px;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
}
}
.board-card-header {
display: flex;
- min-height: 20px;
-
- .board-card-assignee {
- display: flex;
- justify-content: flex-end;
- position: absolute;
- right: 15px;
- height: 20px;
- width: 20px;
+}
- .avatar-counter {
- display: none;
- vertical-align: middle;
- min-width: 20px;
- line-height: 19px;
- height: 20px;
- padding-left: 2px;
- padding-right: 2px;
- border-radius: 2em;
- }
+.board-card-assignee {
+ display: flex;
+ margin-top: -$gl-padding-4;
+ margin-bottom: -$gl-padding-4;
+
+ .avatar-counter {
+ vertical-align: middle;
+ line-height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ background-color: $gl-text-color-tertiary;
+ font-size: $gl-font-size-xs;
+ cursor: help;
+ font-weight: $gl-font-weight-bold;
+ margin-left: -$gl-padding-4;
+ border: 0;
+ padding: 0 $gl-padding-4;
- img {
- vertical-align: top;
+ @include media-breakpoint-down(md) {
+ min-width: auto;
+ height: $gl-padding;
+ border-radius: $gl-padding;
+ line-height: $gl-padding;
}
+ }
- a {
- position: relative;
- margin-left: -15px;
- }
+ img {
+ vertical-align: top;
+ }
- a:nth-child(1) {
- z-index: 3;
- }
+ .user-avatar-link:not(:only-child) {
+ margin-left: -$gl-padding-4;
- a:nth-child(2) {
+ &:nth-of-type(1) {
z-index: 2;
}
- a:nth-child(3) {
+ &:nth-of-type(2) {
z-index: 1;
}
+ }
- a:nth-child(4) {
- display: none;
- }
-
- &:hover {
- .avatar-counter {
- display: inline-block;
- }
-
- a {
- position: static;
- background-color: $white-light;
- transition: background-color 0s;
- margin-left: auto;
-
- &:nth-child(4) {
- display: block;
- }
+ .avatar {
+ margin: 0;
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $white-light;
- }
- }
+ @include media-breakpoint-down(md) {
+ width: $gl-padding;
+ height: $gl-padding;
}
}
- .avatar {
- margin: 0;
+ @include media-breakpoint-down(md) {
+ margin-top: 0;
+ margin-bottom: 0;
}
}
-.board-card-footer {
- margin: 0 0 5px;
+.board-card-number {
+ font-size: $gl-font-size-xs;
+ color: $gl-text-color-secondary;
+ overflow: hidden;
- .badge {
- margin-top: 5px;
- margin-right: 6px;
+ @include media-breakpoint-up(md) {
+ font-size: $label-font-size;
}
}
-.board-card-number {
- font-size: 12px;
- color: $gl-text-color-secondary;
+.board-card-number-container {
+ overflow: hidden;
}
.issue-boards-search {
@@ -474,8 +477,7 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $sidebar-transition-duration,
- padding $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
@@ -650,3 +652,36 @@
}
}
}
+
+.board-card-info {
+ color: $gl-text-color-secondary;
+ white-space: nowrap;
+ margin-right: $gl-padding-8;
+
+ &:not(.board-card-weight) {
+ cursor: help;
+ }
+
+ &.board-card-weight {
+ color: $gl-text-color;
+ cursor: pointer;
+
+ &:hover {
+ color: initial;
+ text-decoration: underline;
+ }
+ }
+
+ .board-card-info-icon {
+ color: $theme-gray-600;
+ margin-right: $gl-padding-4;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
+ }
+}
+
+.board-issue-path.js-show-tooltip {
+ cursor: help;
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 71a3fd544f2..ad12cd101b6 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -25,6 +25,12 @@
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
+
+ &:last-child {
+ border-bottom: 0;
+ border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
+ border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
+ }
}
}
@@ -73,6 +79,10 @@
padding: $gl-padding-top $gl-padding;
}
+ .card {
+ margin-bottom: $gl-vert-padding;
+ }
+
.empty-state .svg-content img {
width: 145px;
}
@@ -80,6 +90,31 @@
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
+
+ .clusters-table {
+ background-color: $gray-light;
+ padding: $gl-padding-8;
+ }
+
+ .badge-light {
+ background-color: $white-normal;
+ }
+
+ .gl-responsive-table-row {
+ padding: $gl-padding;
+ border: 0;
+
+ &.table-row-header {
+ background-color: none;
+ border: 0;
+ font-weight: bold;
+ color: $gl-gray-500;
+ }
+ }
+}
+
+.cluster-warning {
+ @include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
}
.gcp-signup-offer {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 52c91266ff4..6d998fa1e07 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,6 +59,7 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-line-num {
width: 50px;
@@ -421,21 +422,13 @@
.diff-file-container {
.frame.deleted {
- border: 0;
+ border: 1px solid $deleted;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $deleted;
- }
}
.frame.added {
- border: 0;
+ border: 1px solid $added;
background-color: inherit;
-
- .image_file img {
- border: 1px solid $added;
- }
}
.swipe.view,
@@ -481,6 +474,11 @@
bottom: -25px;
}
}
+
+ .discussion-notes .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+ }
}
.file-content .diff-file {
@@ -804,7 +802,7 @@
// double jagged line divider
.discussion-notes + .discussion-notes::before,
- .discussion-notes + .discussion-form::before {
+ .diff-file-discussions + .discussion-form::before {
content: '';
position: relative;
display: block;
@@ -844,6 +842,13 @@
background-repeat: repeat;
}
+ .diff-file-discussions + .discussion-form::before {
+ width: auto;
+ margin-left: -16px;
+ margin-right: -16px;
+ margin-bottom: 16px;
+ }
+
.notes {
position: relative;
}
@@ -855,7 +860,7 @@
}
.diff-file .note-container > .new-note,
-.note-container .discussion-notes {
+.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
@@ -870,11 +875,13 @@
}
}
-.files:not([data-can-create-note]) .frame {
+.files:not([data-can-create-note="true"]) .frame {
cursor: auto;
}
-.frame.click-to-comment {
+.frame,
+.frame.click-to-comment,
+.btn-transparent.image-diff-overlay-add-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
@@ -910,6 +917,7 @@
.frame .badge.badge-pill,
.image-diff-avatar-link .badge.badge-pill,
+.user-avatar-link .badge.badge-pill,
.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
@@ -944,7 +952,8 @@
}
}
-.image-diff-avatar-link {
+.image-diff-avatar-link,
+.user-avatar-link {
position: relative;
.badge.badge-pill,
@@ -1073,3 +1082,14 @@
top: 0;
}
}
+
+.image-diff-overlay,
+.image-diff-overlay-add-comment {
+ top: 0;
+ left: 0;
+
+ &:active,
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c60bb360a03..855d73a9939 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -239,6 +239,7 @@
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
&.is-replying {
padding-bottom: $gl-padding;
@@ -247,10 +248,15 @@
}
.discussion-with-resolve-btn {
+ @include media-breakpoint-up(sm) {
+ display: flex;
+ }
+
+
.discussion-actions {
display: table;
- .btn-default path {
+ svg {
fill: $gray-darkest;
}
@@ -270,6 +276,12 @@
.btn {
width: 100%;
}
+
+ .btn-text-field {
+ @include media-breakpoint-down(xs) {
+ margin-bottom: $gl-padding-8;
+ }
+ }
}
.discussion-notes-count {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index be535ade0a6..dcb1275d182 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,26 +1,135 @@
-/**
- * Notes
- */
+$system-note-icon-size: 32px;
+$system-note-svg-size: 16px;
+$note-form-margin-left: 70px;
-@-webkit-keyframes targe3-note {
- from {
- background: $note-targe3-outside;
+@mixin vertical-line($left) {
+ &::before {
+ content: '';
+ border-left: 2px solid $theme-gray-100;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: $left;
}
+}
- 50% {
- background: $note-targe3-inside;
- }
+.note-wrapper {
+ padding: $gl-padding;
+}
+
+.issuable-discussion {
+ .notes.timeline > .timeline-entry {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ margin: $gl-padding 0;
+
+ &.system-note,
+ &.note-form {
+ border: 0;
+ }
+
+ &.note-form {
+ margin-left: 0;
- to {
- background: $note-targe3-outside;
+ @include notes-media('min', map-get($grid-breakpoints, md)) {
+ margin-left: $note-form-margin-left;
+ }
+
+ .timeline-icon {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
+ margin-left: -$note-icon-gutter-width;
+ }
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
+
+ .notes_content {
+ border: 0;
+ border-top: 1px solid $border-color;
+ }
}
}
-ul.notes {
+.main-notes-list {
+ @include vertical-line(39px);
+}
+
+.notes {
display: block;
list-style: none;
margin: 0;
padding: 0;
+ position: relative;
+
+ > .note-discussion {
+ .card {
+ border: 0;
+ }
+
+ li.note {
+ border-bottom: 1px solid $border-color;
+
+ &:first-child {
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+ }
+ }
+
+ .replies-toggle {
+ background-color: $gray-light;
+ padding: $gl-padding-8 $gl-padding;
+
+ .collapse-replies-btn:hover {
+ color: $blue-600;
+ }
+
+ &.expanded {
+ border-bottom: 1px solid $border-color;
+
+ span {
+ cursor: pointer;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
+ }
+
+ &.collapsed {
+ color: $gl-text-color-secondary;
+
+ svg {
+ float: left;
+ position: relative;
+ top: $gl-padding-4;
+ margin-right: $gl-padding-8;
+ cursor: pointer;
+ }
+
+ img {
+ margin: -2px 4px 0 0;
+ }
+
+ .author-link {
+ color: $gl-text-color;
+ }
+ }
+
+ .user-avatar-link {
+ &:last-child img {
+ margin-right: $gl-padding-8;
+ }
+ }
+
+ .btn-link {
+ border: 0;
+ vertical-align: baseline;
+ }
+ }
.note-created-ago,
.note-updated-at {
@@ -28,8 +137,6 @@ ul.notes {
}
.discussion-body {
- padding-top: 8px;
-
.card {
margin-bottom: 0;
}
@@ -46,21 +153,10 @@ ul.notes {
}
> li {
- // .timeline-entry
- padding: 0;
display: block;
position: relative;
border-bottom: 0;
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- padding-left: $note-icon-gutter-width;
- }
-
- .timeline-entry-inner {
- padding: $gl-padding $gl-btn-padding;
- border-bottom: 1px solid $white-normal;
- }
-
&:target,
&.target {
border-bottom: 1px solid $white-normal;
@@ -75,23 +171,10 @@ ul.notes {
}
}
- .timeline-icon {
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: -$note-icon-gutter-width;
- }
- }
-
- .timeline-content {
- margin-left: $note-icon-gutter-width;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- }
- }
-
&.being-posted {
pointer-events: none;
opacity: 0.5;
+ padding: $gl-padding;
.dummy-avatar {
background-color: $gl-gray-200;
@@ -104,12 +187,6 @@ ul.notes {
}
}
- &.note-discussion {
- .timeline-entry-inner {
- padding: $gl-padding 10px;
- }
- }
-
.editing-spinner {
display: none;
}
@@ -191,8 +268,9 @@ ul.notes {
}
.system-note {
- font-size: 14px;
- clear: both;
+ padding: 6px $gl-padding-24;
+ margin: $gl-padding-24 0;
+ background-color: transparent;
.note-header-info {
padding-bottom: 0;
@@ -225,17 +303,21 @@ ul.notes {
.timeline-icon {
float: left;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- width: auto;
- }
+ display: flex;
+ align-items: center;
+ background-color: $white-light;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
+ border: 1px solid $border-color;
+ border-radius: $system-note-icon-size;
+ margin: -6px $gl-padding 0 0;
svg {
- width: 16px;
- height: 16px;
+ width: $system-note-svg-size;
+ height: $system-note-svg-size;
fill: $gray-darkest;
- margin-top: 2px;
+ display: block;
+ margin: 0 auto;
}
}
@@ -302,10 +384,17 @@ ul.notes {
.discussion-body .diff-file {
.file-title {
cursor: default;
+ line-height: 42px;
+ padding: 0 $gl-padding;
+ border-top: 1px solid $border-color;
&:hover {
background-color: $gray-light;
}
+
+ .btn-clipboard {
+ top: 10px;
+ }
}
.line_content {
@@ -320,6 +409,23 @@ ul.notes {
}
}
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
+
+ .system-note {
+ margin: 0;
+ padding: $gl-padding;
+ }
+ }
+
// Merge request notes in diffs
// Diff is inline
.notes_content .note-header .note-headline-light {
@@ -335,7 +441,6 @@ ul.notes {
border-left: 0;
&.notes_content {
- background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -349,18 +454,6 @@ ul.notes {
}
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
- }
- }
-
.notes {
background-color: $white-light;
}
@@ -374,6 +467,30 @@ ul.notes {
}
}
+.diffs {
+ .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+
+ .notes {
+ position: relative;
+ @include vertical-line(52px);
+ }
+ }
+
+ .note-wrapper {
+ margin: $gl-padding;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .discussion-reply-holder {
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ border-top: 1px solid $border-color;
+ position: relative;
+ }
+}
+
.discussion-header,
.note-header-info {
a {
@@ -399,7 +516,17 @@ ul.notes {
}
.discussion-header {
- font-size: 14px;
+ min-height: 72px;
+
+ .note-header-info {
+ padding-bottom: 0;
+ }
+}
+
+.unresolved {
+ .note-header-info {
+ margin-top: $gl-padding-8;
+ }
}
.note-header {
@@ -409,7 +536,7 @@ ul.notes {
.note-header-info {
min-width: 0;
- padding-bottom: 8px;
+ padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
@@ -471,9 +598,18 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ margin: $gl-padding-8 0;
+ }
+
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
+
+ svg {
+ height: 15px;
+ }
}
.note-actions {
@@ -585,19 +721,6 @@ ul.notes {
z-index: 10;
}
-.discussion-body,
-.diff-file {
- .notes .note {
- border-bottom: 1px solid $white-normal;
-
- .timeline-entry-inner {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- border-bottom: 0;
- }
- }
-}
-
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -634,7 +757,7 @@ ul.notes {
}
.btn {
- svg path {
+ svg {
fill: $gray-darkest;
}
@@ -659,7 +782,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 0dd7500623d..7f874687212 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -100,12 +100,18 @@ 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)
- serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
+ 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] }
+ }
+ )
end
def whitelist_query_limiting
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
new file mode 100644
index 00000000000..b4f46cddbe9
--- /dev/null
+++ b/app/controllers/chaos_controller.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class ChaosController < ActionController::Base
+ before_action :validate_request
+
+ def leakmem
+ memory_mb = (params[:memory_mb]&.to_i || 100)
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+
+ start = Time.now
+ retainer = []
+ # Add `n` 1mb chunks of memory to the retainer array
+ memory_mb.times { retainer << "x" * 1.megabyte }
+
+ duration_taken = (Time.now - start).seconds
+ Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def cpuspin
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ end_time = Time.now + duration_s.seconds
+
+ rand while Time.now < end_time
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def sleep
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ Kernel.sleep duration_s
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def kill
+ Process.kill("KILL", Process.pid)
+ end
+
+ private
+
+ def validate_request
+ 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
+ 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
+ end
+ end
+end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index f6f2060ebb5..2e9c77ae55c 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -183,13 +183,13 @@ class Clusters::ClustersController < Clusters::BaseController
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
- end
+ end.present(current_user: current_user)
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
- end
+ end.present(current_user: current_user)
end
def validate_gcp_token
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index f644702cbdb..b3777fd2b0f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -86,10 +86,10 @@ module CreatesCommit
def new_merge_request_path
project_new_merge_request_path(
@project_to_commit_into,
- merge_request_source_branch: @branch_name,
merge_request: {
source_project_id: @project_to_commit_into.id,
target_project_id: @project.id,
+ source_branch: @branch_name,
target_branch: @start_branch
}
)
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 0bb7b7efed0..515a9eede8e 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module SendFileUpload
- def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
+ def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
if attachment
# Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for
@@ -17,7 +17,7 @@ module SendFileUpload
if file_upload.file_storage?
send_file file_upload.path, send_params
- elsif file_upload.class.proxy_download_enabled?
+ elsif file_upload.class.proxy_download_enabled? || proxy
headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
head :ok
else
diff --git a/app/controllers/groups/clusters/applications_controller.rb b/app/controllers/groups/clusters/applications_controller.rb
new file mode 100644
index 00000000000..8dd8a01cf40
--- /dev/null
+++ b/app/controllers/groups/clusters/applications_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Groups::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :group
+ requires_cross_project_access
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+end
diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
new file mode 100644
index 00000000000..50c44b7a58b
--- /dev/null
+++ b/app/controllers/groups/clusters_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Groups::ClustersController < Clusters::ClustersController
+ include ControllerWithCrossProjectAccessCheck
+
+ prepend_before_action :check_group_clusters_feature_flag!
+ prepend_before_action :group
+ requires_cross_project_access
+
+ layout 'group'
+
+ private
+
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
+ end
+
+ def group
+ @group ||= find_routable!(Group, params[:group_id] || params[:id])
+ end
+
+ def check_group_clusters_feature_flag!
+ render_404 unless Feature.enabled?(:group_clusters)
+ end
+end
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index fdd1078cdf7..575c40d5f6f 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -54,14 +54,14 @@ class Import::BitbucketServerController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
- repos = bitbucket_client.repos
+ @collection = bitbucket_client.repos(page_offset: page_offset, limit: limit_per_page)
+ @repos, @incompatible_repos = @collection.partition { |repo| repo.valid? }
- @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
-
- @already_added_projects = find_already_added_projects('bitbucket_server')
+ # Use the import URL to filter beyond what BaseService#find_already_added_projects
+ @already_added_projects = filter_added_projects('bitbucket_server', @repos.map(&:browse_url))
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
+ @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e
flash[:alert] = "Unable to connect to server: #{e}"
clear_session_data
@@ -75,6 +75,12 @@ class Import::BitbucketServerController < Import::BaseController
private
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_added_projects(import_type, import_sources)
+ current_user.created_projects.where(import_type: import_type, import_source: import_sources).includes(:import_state)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def bitbucket_client
@bitbucket_client ||= BitbucketServer::Client.new(credentials)
end
@@ -130,4 +136,12 @@ class Import::BitbucketServerController < Import::BaseController
password: session[personal_access_token_key]
}
end
+
+ def page_offset
+ [0, params[:page].to_i].max
+ end
+
+ def limit_per_page
+ BitbucketServer::Paginator::PAGE_LENGTH
+ end
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 312e256ea6c..ae9c17802b9 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -16,7 +16,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def download
return render_404 unless artifacts_file
- send_upload(artifacts_file, attachment: artifacts_file.filename)
+ send_upload(artifacts_file, attachment: artifacts_file.filename, proxy: params[:proxy])
end
def browse
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index d386fb63d9f..9c130af8394 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -1,40 +1,38 @@
# frozen_string_literal: true
class Projects::AutocompleteSourcesController < Projects::ApplicationController
- before_action :load_autocomplete_service, except: [:members]
-
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
def issues
- render json: @autocomplete_service.issues
+ render json: autocomplete_service.issues
end
def merge_requests
- render json: @autocomplete_service.merge_requests
+ render json: autocomplete_service.merge_requests
end
def labels
- render json: @autocomplete_service.labels_as_hash(target)
+ render json: autocomplete_service.labels_as_hash(target)
end
def milestones
- render json: @autocomplete_service.milestones
+ render json: autocomplete_service.milestones
end
def commands
- render json: @autocomplete_service.commands(target, params[:type])
+ render json: autocomplete_service.commands(target, params[:type])
end
def snippets
- render json: @autocomplete_service.snippets
+ render json: autocomplete_service.snippets
end
private
- def load_autocomplete_service
- @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+ def autocomplete_service
+ @autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
end
def target
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index bbf662a63c8..5639402a1e9 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -89,8 +89,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence
-
@merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index f90971bb9f6..d3774746cb8 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,138 +1,149 @@
# frozen_string_literal: true
-# Snippets Finder
+# Finder for retrieving snippets that a user can see, optionally scoped to a
+# project or snippets author.
#
-# Used to filter Snippets collections by a set of params
+# Basic usage:
#
-# Arguments.
+# user = User.find(1)
#
-# current_user - The current user, nil also can be used.
-# params:
-# visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0).
-# project (Project) - Project related.
-# author (User) - Author related.
+# SnippetsFinder.new(user).execute
#
-# params are optional
+# To limit the snippets to a specific project, supply the `project:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, project: project).execute
+#
+# Limiting snippets to an author can be done by supplying the `author:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, author: user).execute
+#
+# To filter snippets using a specific visibility level, you can provide the
+# `scope:` option:
+#
+# user = User.find(1)
+# project = Project.find(1)
+#
+# SnippetsFinder.new(user, author: user, scope: :are_public).execute
+#
+# Valid `scope:` values are:
+#
+# * `:are_private`
+# * `:are_internal`
+# * `:are_public`
+#
+# Any other value will be ignored.
class SnippetsFinder < UnionFinder
- include Gitlab::Allowable
include FinderMethods
- attr_accessor :current_user, :project, :params
+ attr_accessor :current_user, :project, :author, :scope
- def initialize(current_user, params = {})
+ def initialize(current_user = nil, params = {})
@current_user = current_user
- @params = params
@project = params[:project]
- end
-
- def execute
- items = init_collection
- items = by_author(items)
- items = by_visibility(items)
-
- items.fresh
- end
-
- private
-
- def init_collection
- if project.present?
- authorized_snippets_from_project
- else
- authorized_snippets
+ @author = params[:author]
+ @scope = params[:scope].to_s
+
+ if project && author
+ raise(
+ ArgumentError,
+ 'Filtering by both an author and a project is not supported, ' \
+ 'as this finder is not optimised for this use case'
+ )
end
end
- def authorized_snippets_from_project
- if can?(current_user, :read_project_snippet, project)
- if project.team.member?(current_user)
- project.snippets
+ def execute
+ base =
+ if project
+ snippets_for_a_single_project
else
- project.snippets.public_to_user(current_user)
+ snippets_for_multiple_projects
end
- else
- Snippet.none
- end
- end
- # rubocop: disable CodeReuse/ActiveRecord
- def authorized_snippets
- # This query was intentionally converted to a raw one to get it work in Rails 5.0.
- # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
- # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
- Snippet.where("#{feature_available_projects} OR #{not_project_related}")
- .public_or_visible_to_user(current_user)
+ base.with_optional_visibility(visibility_from_scope).fresh
end
- # rubocop: enable CodeReuse/ActiveRecord
- # Returns a collection of projects that is either public or visible to the
- # logged in user.
+ # Produces a query that retrieves snippets from multiple projects.
#
- # A caller must pass in a block to modify individual parts of
- # the query, e.g. to apply .with_feature_available_for_user on top of it.
- # This is useful for performance as we can stick those additional filters
- # at the bottom of e.g. the UNION.
- # rubocop: disable CodeReuse/ActiveRecord
- def projects_for_user
- return yield(Project.public_to_user) unless current_user
-
- # If the current_user is allowed to see all projects,
- # we can shortcut and just return.
- return yield(Project.all) if current_user.full_private_access?
-
- authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
-
- levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
- visible_projects = yield(Project.where(visibility_level: levels))
-
- # We use a UNION here instead of OR clauses since this results in better
- # performance.
- Project.from_union([authorized_projects, visible_projects])
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def feature_available_projects
- # Don't return any project related snippets if the user cannot read cross project
- return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)
-
- projects = projects_for_user do |part|
- part.with_feature_available_for_user(:snippets, current_user)
- end.select(:id)
+ # The resulting query will, depending on the user's permissions, include the
+ # following collections of snippets:
+ #
+ # 1. Snippets that don't belong to any project.
+ # 2. Snippets of projects that are visible to the current user (e.g. snippets
+ # in public projects).
+ # 3. Snippets of projects that the current user is a member of.
+ #
+ # Each collection is constructed in isolation, allowing for greater control
+ # over the resulting SQL query.
+ def snippets_for_multiple_projects
+ queries = [global_snippets]
+
+ if Ability.allowed?(current_user, :read_cross_project)
+ queries << snippets_of_visible_projects
+ queries << snippets_of_authorized_projects if current_user
+ end
- # This query was intentionally converted to a raw one to get it work in Rails 5.0.
- # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
- # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
- "snippets.project_id IN (#{projects.to_sql})"
+ find_union(queries, Snippet)
end
- def not_project_related
- table[:project_id].eq(nil).to_sql
+ def snippets_for_a_single_project
+ Snippet.for_project_with_user(project, current_user)
end
- def table
- Snippet.arel_table
+ def global_snippets
+ snippets_for_author_or_visible_to_user.only_global_snippets
end
- # rubocop: disable CodeReuse/ActiveRecord
- def by_visibility(items)
- visibility = params[:visibility] || visibility_from_scope
+ # Returns the snippets that the current user (logged in or not) can view.
+ def snippets_of_visible_projects
+ snippets_for_author_or_visible_to_user
+ .only_include_projects_visible_to(current_user)
+ .only_include_projects_with_snippets_enabled
+ end
- return items unless visibility
+ # Returns the snippets that the currently logged in user has access to by
+ # being a member of the project the snippets belong to.
+ #
+ # This method requires that `current_user` returns a `User` instead of `nil`,
+ # and is optimised for this specific scenario.
+ def snippets_of_authorized_projects
+ base = author ? snippets_for_author : Snippet.all
+
+ base
+ .only_include_projects_with_snippets_enabled(include_private: true)
+ .only_include_authorized_projects(current_user)
+ end
- items.where(visibility_level: visibility)
+ def snippets_for_author_or_visible_to_user
+ if author
+ snippets_for_author
+ elsif current_user
+ Snippet.visible_to_or_authored_by(current_user)
+ else
+ Snippet.public_to_user
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def by_author(items)
- return items unless params[:author]
+ def snippets_for_author
+ base = author.snippets
- items.where(author_id: params[:author].id)
+ if author == current_user
+ # If the current user is also the author of all snippets, then we can
+ # include private snippets.
+ base
+ else
+ base.public_to_user(current_user)
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
def visibility_from_scope
- case params[:scope].to_s
+ case scope
when 'are_private'
Snippet::PRIVATE
when 'are_internal'
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 9a1c2a4c9e1..086bb38ce9a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -217,7 +217,8 @@ module ApplicationSettingsHelper
:user_oauth_applications,
:version_check_enabled,
:web_ide_clientside_preview_enabled,
- :diff_max_patch_bytes
+ :diff_max_patch_bytes,
+ :commit_email_hostname
]
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 7fc4c1a023f..5906ddabee4 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -22,7 +22,7 @@ module AvatarsHelper
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
- user = User.find_by_any_email(email.try(:downcase))
+ user = User.find_by_any_email(email)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 57e397f6ca0..9ece8b0bc5b 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -13,8 +13,8 @@ module CompareHelper
def create_mr_path(from = params[:from], to = params[:to], project = @project)
project_new_merge_request_path(
project,
- merge_request_source_branch: to,
merge_request: {
+ source_branch: to,
target_branch: from
}
)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 2adfc04deb8..3ce2398f1de 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -91,7 +91,14 @@ module EventsHelper
words << "##{event.target_iid}" if event.target_iid
words << "in"
elsif event.target
- words << "##{event.target_iid}:"
+ prefix =
+ if event.merge_request?
+ MergeRequest.reference_prefix
+ else
+ Issue.reference_prefix
+ end
+
+ words << "#{prefix}#{event.target_iid}:" if event.target_iid
words << event.target.title if event.target.respond_to?(:title)
words << "at"
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 0c313e9e6d3..e9b9b9b7721 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -140,6 +140,10 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
+ if can?(current_user, :read_cluster, @group) && Feature.enabled?(:group_clusters)
+ links << :kubernetes
+ end
+
if can?(current_user, :admin_group, @group)
links << :settings
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8f549bfce73..23d7aa427bb 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -11,10 +11,10 @@ module MergeRequestsHelper
def new_mr_from_push_event(event, target_project)
{
- merge_request_source_branch: event.branch_name,
merge_request: {
source_project_id: event.project.id,
target_project_id: target_project.id,
+ source_branch: event.branch_name,
target_branch: target_project.repository.root_ref
}
}
@@ -51,10 +51,10 @@ module MergeRequestsHelper
def mr_change_branches_path(merge_request)
project_new_merge_request_path(
@project,
- merge_request_source_branch: merge_request.source_branch,
merge_request: {
source_project_id: merge_request.source_project_id,
target_project_id: merge_request.target_project_id,
+ source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch
},
change_branches: true
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 55674e37a34..42f9a1213e9 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -1,6 +1,20 @@
# frozen_string_literal: true
module ProfilesHelper
+ def commit_email_select_options(user)
+ private_email = user.private_commit_email
+ verified_emails = user.verified_emails - [private_email]
+
+ [
+ [s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN],
+ verified_emails
+ ]
+ end
+
+ def selected_commit_email(user)
+ user.read_attribute(:commit_email) || user.commit_email
+ end
+
def attribute_provider_label(attribute)
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d9713f9c9b0..0a7f930110a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -540,4 +540,13 @@ module ProjectsHelper
network
]
end
+
+ def sidebar_operations_paths
+ %w[
+ environments
+ clusters
+ user
+ gcp
+ ]
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 704310f53f0..207ffae873a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -187,6 +187,8 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true
+ validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
+
validates :archive_builds_in_seconds,
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
@@ -299,10 +301,15 @@ class ApplicationSetting < ActiveRecord::Base
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil,
- diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES
+ diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
+ commit_email_hostname: default_commit_email_hostname
}
end
+ def self.default_commit_email_hostname
+ "users.noreply.#{Gitlab.config.gitlab.host}"
+ end
+
def self.create_from_defaults
create(defaults)
end
@@ -358,6 +365,10 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
+ def commit_email_hostname
+ super.presence || self.class.default_commit_email_hostname
+ end
+
def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level))
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 360c9924a7d..889f8ce27a6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -211,6 +211,7 @@ module Ci
build.deployment&.succeed
build.run_after_commit do
+ BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
@@ -220,9 +221,7 @@ module Ci
build.deployment&.drop
- next if build.retries_max.zero?
-
- if build.retries_count < build.retries_max
+ if build.retry_failure?
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
@@ -320,7 +319,17 @@ module Ci
end
def retries_max
- self.options.to_h.fetch(:retry, 0).to_i
+ normalized_retry.fetch(:max, 0)
+ end
+
+ def retry_when
+ normalized_retry.fetch(:when, ['always'])
+ end
+
+ def retry_failure?
+ return false if retries_max.zero? || retries_count >= retries_max
+
+ retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
end
def latest?
@@ -815,7 +824,7 @@ module Ci
end
end
- def predefined_variables
+ def predefined_variables # rubocop:disable Metrics/AbcSize
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
@@ -835,6 +844,8 @@ module Ci
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
+ variables.append(key: "CI_NODE_INDEX", value: self.options[:instance].to_s) if self.options&.include?(:instance)
+ variables.append(key: "CI_NODE_TOTAL", value: (self.options&.dig(:parallel) || 1).to_s)
variables.concat(legacy_variables)
end
end
@@ -883,6 +894,16 @@ module Ci
options&.dig(:environment, :url) || persisted_environment&.external_url
end
+ # The format of the retry option changed in GitLab 11.5: Before it was
+ # integer only, after it is a hash. New builds are created with the new
+ # format, but builds created before GitLab 11.5 and saved in database still
+ # have the old integer only format. This method returns the retry option
+ # normalized as a hash in 11.5+ format.
+ def normalized_retry
+ value = options&.dig(:retry)
+ value.is_a?(Integer) ? { max: value } : value.to_h
+ end
+
def build_attributes_from_config
return {} unless pipeline.config_processor
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
new file mode 100644
index 00000000000..a79a97576d1
--- /dev/null
+++ b/app/models/clusters/applications/knative.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class Knative < ActiveRecord::Base
+ VERSION = '0.1.3'.freeze
+ REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
+
+ # This is required for helm version <= 2.10.x in order to support
+ # Setting up CRDs
+ ISTIO_CRDS = 'https://storage.googleapis.com/triggermesh-charts/istio-crds.yaml'.freeze
+
+ self.table_name = 'clusters_applications_knative'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ validates :hostname, presence: true, hostname: true
+
+ def chart
+ 'knative/knative'
+ end
+
+ def values
+ { "domain" => hostname }.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: name,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files,
+ repository: REPOSITORY,
+ preinstall: install_script
+ )
+ end
+
+ private
+
+ def install_script
+ ["/usr/bin/kubectl apply -f #{ISTIO_CRDS}"]
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e80d35d0f3c..0ba056e57d4 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -12,7 +12,8 @@ module Clusters
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter
+ Applications::Jupyter.application_name => Applications::Jupyter,
+ Applications::Knative.application_name => Applications::Knative
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
@@ -28,13 +29,14 @@ module Clusters
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
- has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true
+ has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
+ has_one :application_knative, class_name: 'Clusters::Applications::Knative'
has_many :kubernetes_namespaces
has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace'
@@ -100,7 +102,8 @@ module Clusters
application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus,
application_runner || build_application_runner,
- application_jupyter || build_application_jupyter
+ application_jupyter || build_application_jupyter,
+ application_knative || build_application_knative
]
end
@@ -141,6 +144,10 @@ module Clusters
)
end
+ def allow_user_defined_namespace?
+ project_type?
+ end
+
private
def restrict_modification
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 008e08d9914..ea02ae6c9d8 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -26,6 +26,7 @@ module Clusters
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
+ before_validation :enforce_ca_whitespace_trimming
validates :namespace,
allow_blank: true,
@@ -37,6 +38,8 @@ module Clusters
validates :namespace, exclusion: { in: RESERVED_NAMESPACES }
+ validate :no_namespace, unless: :allow_user_defined_namespace?
+
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
@@ -51,6 +54,7 @@ module Clusters
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
delegate :managed?, to: :cluster, allow_nil: true
+ delegate :allow_user_defined_namespace?, to: :cluster, allow_nil: true
delegate :kubernetes_namespace, to: :cluster
alias_method :active?, :enabled?
@@ -149,7 +153,8 @@ module Clusters
end
def build_kube_client!
- raise "Incomplete settings" unless api_url && actual_namespace
+ raise "Incomplete settings" unless api_url
+ raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace
unless (username && password) || token
raise "Either username/password or token is required to access API"
@@ -201,6 +206,17 @@ module Clusters
self.namespace = self.namespace&.downcase
end
+ def enforce_ca_whitespace_trimming
+ self.ca_pem = self.ca_pem&.strip
+ self.token = self.token&.strip
+ end
+
+ def no_namespace
+ if namespace
+ errors.add(:namespace, 'only allowed for project cluster')
+ end
+ end
+
def prevent_modification
return unless managed?
diff --git a/app/models/commit.rb b/app/models/commit.rb
index a61ed03cf35..9dd0cbacd9e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -260,7 +260,7 @@ class Commit
request_cache(:author) { author_email.downcase }
def committer
- @committer ||= User.find_by_any_email(committer_email.downcase)
+ @committer ||= User.find_by_any_email(committer_email)
end
def parents
diff --git a/app/models/compare.rb b/app/models/compare.rb
index b2d46ada831..f1ed84ab5a5 100644
--- a/app/models/compare.rb
+++ b/app/models/compare.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'set'
+
class Compare
include Gitlab::Utils::StrongMemoize
@@ -77,4 +79,13 @@ class Compare
head_sha: head_commit_sha
)
end
+
+ def modified_paths
+ paths = Set.new
+ diffs.diff_files.each do |diff|
+ paths.add diff.old_path
+ paths.add diff.new_path
+ end
+ paths.to_a
+ end
end
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index 8cf0b8b154d..6314b46a7e3 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -39,7 +39,15 @@ module EachBatch
#
# of - The number of rows to retrieve per batch.
# column - The column to use for ordering the batches.
- def each_batch(of: 1000, column: primary_key)
+ # order_hint - An optional column to append to the `ORDER BY id`
+ # clause to help the query planner. PostgreSQL might perform badly
+ # with a LIMIT 1 because the planner is guessing that scanning the
+ # index in ID order will come across the desired row in less time
+ # it will take the planner than using another index. The
+ # order_hint does not affect the search results. For example,
+ # `ORDER BY id ASC, updated_at ASC` means the same thing as `ORDER
+ # BY id ASC`.
+ def each_batch(of: 1000, column: primary_key, order_hint: nil)
unless column
raise ArgumentError,
'the column: argument must be set to a column name to use for ordering rows'
@@ -48,7 +56,9 @@ module EachBatch
start = except(:select)
.select(column)
.reorder(column => :asc)
- .take
+
+ start = start.order(order_hint) if order_hint
+ start = start.take
return unless start
@@ -60,6 +70,9 @@ module EachBatch
.select(column)
.where(arel_table[column].gteq(start_id))
.reorder(column => :asc)
+
+ stop = stop.order(order_hint) if order_hint
+ stop = stop
.offset(of)
.limit(1)
.take
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 54a900a3b85..83434276995 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -10,7 +10,9 @@ class Deployment < ActiveRecord::Base
belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
- has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
+ has_internal_id :iid, scope: :project, init: ->(s) do
+ Deployment.where(project: s.project).maximum(:iid) if s&.project
+ end
validates :sha, presence: true
validates :ref, presence: true
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 7d104bb0c25..934828946b9 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -50,6 +50,7 @@ class Environment < ActiveRecord::Base
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
scope :for_project, -> (project) { where(project_id: project) }
+ scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
state_machine :state, initial: :available do
event :start do
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 7efc8da09ad..7078496ff52 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -8,17 +8,16 @@ 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.head_pipeline)
+ build_environments_status(mr, user, mr.diff_head_sha)
end
def self.after_merge_request(mr, user)
return [] unless mr.merged?
- build_environments_status(mr, user, mr.merge_pipeline)
+ build_environments_status(mr, user, mr.merge_commit_sha)
end
def initialize(environment, merge_request, sha)
@@ -29,7 +28,7 @@ class EnvironmentStatus
def deployment
strong_memoize(:deployment) do
- environment.first_deployment_for(sha)
+ Deployment.where(environment: environment).find_by_sha(sha)
end
end
@@ -44,6 +43,22 @@ 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
@@ -61,21 +76,14 @@ class EnvironmentStatus
}
end
- def self.build_environments_status(mr, user, pipeline)
- return [] unless pipeline.present?
+ 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|
+ next unless Ability.allowed?(user, :read_environment, environment)
- find_environments(user, pipeline).map do |environment|
- EnvironmentStatus.new(environment, mr, pipeline.sha)
- end
+ EnvironmentStatus.new(environment, mr, sha)
+ end.compact
end
private_class_method :build_environments_status
-
- def self.find_environments(user, pipeline)
- env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id)
-
- Environment.available.where(id: env_ids).select do |environment|
- Ability.allowed?(user, :read_environment, environment)
- end
- end
- private_class_method :find_environments
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index abdb3448d4e..0de5e434b02 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -231,6 +231,20 @@ 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/merge_request.rb b/app/models/merge_request.rb
index 735d9fba966..df5678ec2f1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -409,6 +409,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff&.real_size || diffs.real_size
end
+ def modified_paths(past_merge_request_diff: nil)
+ diffs = if past_merge_request_diff
+ past_merge_request_diff
+ elsif compare
+ compare
+ else
+ self.merge_request_diff
+ end
+
+ diffs.modified_paths
+ end
+
def diff_base_commit
if persisted?
merge_request_diff.base_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index bb6ff8921df..74583af1a29 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,6 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base
include ManualInverseAssociation
include IgnorableColumn
include EachBatch
+ include Gitlab::Utils::StrongMemoize
# Don't display more than 100 commits at once
COMMITS_SAFE_SIZE = 100
@@ -234,6 +235,12 @@ class MergeRequestDiff < ActiveRecord::Base
end
# rubocop: enable CodeReuse/ServiceClass
+ def modified_paths
+ strong_memoize(:modified_paths) do
+ merge_request_diff_files.pluck(:new_path, :old_path).flatten.uniq
+ end
+ end
+
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/project.rb b/app/models/project.rb
index d5a4ae79c47..48905547ab4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2073,6 +2073,10 @@ class Project < ActiveRecord::Base
storage_version != LATEST_STORAGE_VERSION
end
+ def snippets_visible?(user = nil)
+ Ability.allowed?(user, :read_project_snippet, self)
+ end
+
private
def use_hashed_storage
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index d59cb43dea4..7126bb66d80 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -56,4 +56,17 @@ class ProjectImportState < ActiveRecord::Base
end
end
end
+
+ def mark_as_failed(error_message)
+ original_errors = errors.dup
+ sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
+
+ fail_op
+
+ 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
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ee5579329a8..6e179f61a7b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1014,6 +1014,18 @@ class Repository
message: merge_request.title)
end
+ def update_submodule(user, submodule, commit_sha, message:, branch:)
+ with_cache_hooks do
+ raw.update_submodule(
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ )
+ end
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 1c5846b4023..11856b55902 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -63,6 +63,62 @@ class Snippet < ActiveRecord::Base
attr_spammable :title, spam_title: true
attr_spammable :content, spam_description: true
+ def self.with_optional_visibility(value = nil)
+ if value
+ where(visibility_level: value)
+ else
+ all
+ end
+ end
+
+ def self.only_global_snippets
+ where(project_id: nil)
+ end
+
+ def self.only_include_projects_visible_to(current_user = nil)
+ levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
+
+ joins(:project).where('projects.visibility_level IN (?)', levels)
+ end
+
+ def self.only_include_projects_with_snippets_enabled(include_private: false)
+ column = ProjectFeature.access_level_attribute(:snippets)
+ levels = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+
+ levels << ProjectFeature::PRIVATE if include_private
+
+ joins(project: :project_feature)
+ .where(project_features: { column => levels })
+ end
+
+ def self.only_include_authorized_projects(current_user)
+ where(
+ 'EXISTS (?)',
+ ProjectAuthorization
+ .select(1)
+ .where('project_id = snippets.project_id')
+ .where(user_id: current_user.id)
+ )
+ end
+
+ def self.for_project_with_user(project, user = nil)
+ return none unless project.snippets_visible?(user)
+
+ if user && project.team.member?(user)
+ project.snippets
+ else
+ project.snippets.public_to_user(user)
+ end
+ end
+
+ def self.visible_to_or_authored_by(user)
+ where(
+ 'snippets.visibility_level IN (?) OR snippets.author_id = ?',
+ Gitlab::VisibilityLevel.levels_for_user(user),
+ user.id
+ )
+ end
+
def self.reference_prefix
'$'
end
@@ -81,27 +137,6 @@ class Snippet < ActiveRecord::Base
@link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end
- # Returns a collection of snippets that are either public or visible to the
- # logged in user.
- #
- # This method does not verify the user actually has the access to the project
- # the snippet is in, so it should be only used on a relation that's already scoped
- # for project access
- def self.public_or_visible_to_user(user = nil)
- if user
- authorized = user
- .project_authorizations
- .select(1)
- .where('project_authorizations.project_id = snippets.project_id')
-
- levels = Gitlab::VisibilityLevel.levels_for_user(user)
-
- where('EXISTS (?) OR snippets.visibility_level IN (?) or snippets.author_id = (?)', authorized, levels, user.id)
- else
- public_to_user
- end
- end
-
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{id}"
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 23bc9ca42fc..e01e9c6a4f0 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -11,7 +11,8 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
- scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+ scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
+ scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
@@ -46,7 +47,18 @@ class Upload < ActiveRecord::Base
end
def exist?
- File.exist?(absolute_path)
+ exist = File.exist?(absolute_path)
+
+ # Help sysadmins find missing upload files
+ if persisted? && !exist
+ if Gitlab::Sentry.enabled?
+ Raven.capture_message("Upload file does not exist", extra: self.attributes)
+ end
+
+ Gitlab::Metrics.counter(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').increment
+ end
+
+ exist
end
def uploader_context
@@ -57,8 +69,6 @@ class Upload < ActiveRecord::Base
end
def local?
- return true if store.nil?
-
store == ObjectStorage::Store::LOCAL
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 039a3854edb..a400058e87e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -347,7 +347,11 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email, confirmed: false)
- by_any_email(email, confirmed: confirmed).take
+ return unless email
+
+ downcased = email.downcase
+
+ find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take
end
# Returns a relation containing all the users for the given Email address
@@ -361,6 +365,12 @@ class User < ActiveRecord::Base
from_union([users, emails])
end
+ def find_by_private_commit_email(email)
+ user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)
+
+ find_by(id: user_id)
+ end
+
def filter(filter_name)
case filter_name
when 'admins'
@@ -633,6 +643,10 @@ class User < ActiveRecord::Base
def commit_email
return self.email unless has_attribute?(:commit_email)
+ if super == Gitlab::PrivateCommitEmail::TOKEN
+ return private_commit_email
+ end
+
# The commit email is the same as the primary email if undefined
super.presence || self.email
end
@@ -645,6 +659,10 @@ class User < ActiveRecord::Base
has_attribute?(:commit_email) && super
end
+ def private_commit_email
+ Gitlab::PrivateCommitEmail.for_user(self)
+ end
+
# see if the new email is already a verified secondary email
def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
@@ -1020,13 +1038,21 @@ class User < ActiveRecord::Base
def verified_emails
verified_emails = []
verified_emails << email if primary_email_verified?
+ verified_emails << private_commit_email
verified_emails.concat(emails.confirmed.pluck(:email))
verified_emails
end
def verified_email?(check_email)
downcased = check_email.downcase
- email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
+
+ if email == downcased
+ primary_email_verified?
+ else
+ user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
+
+ user_id == id || emails.confirmed.where(email: downcased).exists?
+ end
end
def hook_attrs
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index c5e349ae913..7769c3d71c0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -151,12 +151,6 @@ class WikiPage
last_version&.sha
end
- # Returns the Date that this latest version was
- # created on.
- def created_at
- @page.version.date
- end
-
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
@@ -195,7 +189,7 @@ class WikiPage
update_attributes(attrs)
save(page_details: title) do
- wiki.create_page(title, content, format, message)
+ wiki.create_page(title, content, format, attrs[:message])
end
end
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index 147943a3d6c..d6d590687e2 100644
--- a/app/policies/clusters/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -4,11 +4,7 @@ module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
+ delegate { cluster.group }
delegate { cluster.project }
-
- rule { can?(:maintainer_access) }.policy do
- enable :update_cluster
- enable :admin_cluster
- end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 73c93b22c95..6b4e56ef5e4 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -65,6 +65,10 @@ class GroupPolicy < BasePolicy
enable :create_projects
enable :admin_pipeline
enable :admin_build
+ enable :read_cluster
+ enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
end
rule { owner }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a76a083bceb..1c082945299 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -258,6 +258,8 @@ class ProjectPolicy < BasePolicy
enable :update_pages
enable :read_cluster
enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
enable :create_environment_terminal
end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index cff0e74d6ea..9cc137aa3bd 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -43,4 +43,16 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
def cluster_path(cluster, params = {})
raise NotImplementedError
end
+
+ def empty_state_help_text
+ nil
+ end
+
+ def sidebar_text
+ raise NotImplementedError
+ end
+
+ def learn_more_link
+ raise NotImplementedError
+ end
end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 78d632eb77c..7e6eccb648c 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -15,6 +15,8 @@ module Clusters
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
+ elsif cluster.group_type?
+ group_cluster_path(group, cluster)
else
raise NotImplementedError
end
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
new file mode 100644
index 00000000000..d963c188559
--- /dev/null
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class GroupClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :install_applications_cluster_path
+ def install_applications_cluster_path(cluster, application)
+ install_applications_group_cluster_path(clusterable, cluster, application)
+ end
+
+ override :cluster_path
+ def cluster_path(cluster, params = {})
+ group_cluster_path(clusterable, cluster, params)
+ end
+
+ override :empty_state_help_text
+ def empty_state_help_text
+ s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')
+ end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster.')
+ end
+
+ 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')
+ end
+end
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 12077b2e735..63e69b91b11 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -1,15 +1,31 @@
# frozen_string_literal: true
class ProjectClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params)
end
+ override :install_applications_cluster_path
def install_applications_cluster_path(cluster, application)
install_applications_project_cluster_path(clusterable, cluster, application)
end
+ override :cluster_path
def cluster_path(cluster, params = {})
project_cluster_path(clusterable, cluster, params)
end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
end
diff --git a/app/serializers/README.md b/app/serializers/README.md
index bb94745b0b5..0337f88db5f 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)
- end
+ nd
end
```
@@ -196,7 +196,7 @@ def index
.represent_details(@project.resources),
count: @project.resources.count
}
- end
+ nd
end
```
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
deleted file mode 100644
index 6a9e9638e70..00000000000
--- a/app/serializers/issue_board_entity.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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 :weight, if: -> (*) { respond_to?(:weight) }
-
- 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 d66f0a5acb7..37cf5e28396 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -4,17 +4,15 @@ 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(issue, opts = {})
+ def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
IssueSidebarEntity
- when 'board'
- IssueBoardEntity
else
IssueEntity
end
- super(issue, opts, entity)
+ super(merge_request, opts, entity)
end
end
diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb
index 5082245dda9..98743d62b50 100644
--- a/app/serializers/label_entity.rb
+++ b/app/serializers/label_entity.rb
@@ -12,8 +12,4 @@ 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/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 5017fa093f3..19dc0478591 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -14,7 +14,8 @@ module Clusters
else
check_timeout
end
- rescue Kubeclient::HttpError
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
app.make_errored!("Kubernetes error") unless app.errored?
end
@@ -51,7 +52,8 @@ module Clusters
def remove_installation_pod
helm_api.delete_pod!(install_command.pod_name)
- rescue
+ rescue => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
# no-op
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index 55f917798de..844807c2581 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -42,10 +42,20 @@ module Clusters
def builders
{
"helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm },
- "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress },
+ "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }
+ }.tap do |hash|
+ hash.merge!(project_builders) if cluster.project_type?
+ end
+ end
+
+ # These applications will need extra configuration to enable them to work
+ # with groups of projects
+ def project_builders
+ {
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
- "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }
+ "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
+ "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative }
}
end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index dd8d2ed5eb6..5a24d78e712 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -12,9 +12,11 @@ module Clusters
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
app.make_errored!("Kubernetes error.")
- rescue StandardError
+ rescue StandardError => e
+ Rails.logger.error "Can't start installation process: #{e.class.name} #{e.message}"
app.make_errored!("Can't start installation process.")
end
end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 270db4a52fd..5a9da053780 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -36,6 +36,10 @@ module Clusters
case clusterable
when ::Project
{ cluster_type: :project_type, projects: [clusterable] }
+ when ::Group
+ { cluster_type: :group_type, groups: [clusterable] }
+ else
+ raise NotImplementedError
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 2fbd442fc2e..fbf71f02837 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -24,8 +24,12 @@ module Commits
start_project: @start_project,
start_branch_name: @start_branch)
rescue Gitlab::Git::Repository::CreateTreeError
- error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
- This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
+ act = action.to_s.dasherize
+ type = @commit.change_type_title(current_user)
+
+ error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \
+ "This #{type} may already have been #{act}ed, or a more recent " \
+ "commit may have updated some of its content."
raise ChangeError, error_msg
end
end
diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb
new file mode 100644
index 00000000000..9253cfaac20
--- /dev/null
+++ b/app/services/commits/commit_patch_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Commits
+ class CommitPatchService < CreateService
+ # Requires:
+ # - project: `Project` to be committed into
+ # - user: `User` that will be the committer
+ # - params:
+ # - branch_name: `String` the branch that will be committed into
+ # - start_branch: `String` the branch that will will started from
+ # - patches: `Gitlab::Git::Patches::Collection` that contains the patches
+ def initialize(*args)
+ super
+
+ @patches = Gitlab::Git::Patches::Collection.new(Array(params[:patches]))
+ end
+
+ private
+
+ def new_branch?
+ !repository.branch_exists?(@branch_name)
+ end
+
+ def create_commit!
+ if @start_branch && new_branch?
+ prepare_branch!
+ end
+
+ Gitlab::Git::Patches::CommitPatches
+ .new(current_user, project.repository, @branch_name, @patches)
+ .commit
+ end
+
+ def prepare_branch!
+ branch_result = CreateBranchService.new(project, current_user)
+ .execute(@branch_name, @start_branch)
+
+ if branch_result[:status] != :success
+ raise ChangeError, branch_result[:message]
+ end
+ end
+
+ # Overridden from the Commits::CreateService, to skip some validations we
+ # don't need:
+ # - validate_on_branch!
+ # Not needed, the patches are applied on top of HEAD if the branch did not
+ # exist
+ # - validate_branch_existence!
+ # Not needed because we continue applying patches on the branch if it
+ # already existed, and create it if it did not exist.
+ def validate!
+ validate_patches!
+ validate_new_branch_name! if new_branch?
+ validate_permissions!
+ end
+
+ def validate_patches!
+ raise_error("Patches are too big") unless @patches.valid_size?
+ end
+ end
+end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index 3ce9acc833c..34593e12bd5 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -19,7 +19,12 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex
+ rescue ValidationError,
+ ChangeError,
+ Gitlab::Git::Index::IndexError,
+ Gitlab::Git::CommitError,
+ Gitlab::Git::PreReceiveError,
+ Gitlab::Git::CommandError => ex
error(ex.message)
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index c388913ae65..e32e262ac31 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -126,12 +126,12 @@ class IssuableBaseService < BaseService
merge_quick_actions_into_params!(issuable)
end
- def merge_quick_actions_into_params!(issuable)
+ def merge_quick_actions_into_params!(issuable, only: nil)
original_description = params.fetch(:description, issuable.description)
description, command_params =
QuickActions::InterpretService.new(project, current_user)
- .execute(original_description, issuable)
+ .execute(original_description, issuable, only: only)
# Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if description
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 0e76d2cc3ab..6c69452e2ab 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,8 +6,12 @@ module MergeRequests
def execute
@params_issue_iid = params.delete(:issue_iid)
+ self.merge_request = MergeRequest.new
+ # TODO: this should handle all quick actions that don't have side effects
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/53658
+ merge_quick_actions_into_params!(merge_request, only: [:target_branch])
+ merge_request.assign_attributes(params)
- self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = []
merge_request.source_project = find_source_project
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 35a22449e34..7c88c9abb41 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -50,8 +50,8 @@ module MergeRequests
end
def url_for_new_merge_request(branch_name)
- url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name)
-
+ merge_request_params = { source_branch: branch_name }
+ url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
{ branch_name: branch_name, url: url, new_merge_request: true }
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 53768ff2cbe..5fe48da1cd6 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -2,18 +2,18 @@
module MergeRequests
class RefreshService < MergeRequests::BaseService
+ attr_reader :push
+
def execute(oldrev, newrev, ref)
- push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
- return true unless push.branch_push?
+ @push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref)
+ return true unless @push.branch_push?
- refresh_merge_requests!(push)
+ refresh_merge_requests!
end
private
- def refresh_merge_requests!(push)
- @push = push
-
+ def refresh_merge_requests!
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index eb431c36807..9c81de7e90e 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -23,13 +23,13 @@ module QuickActions
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
- def execute(content, issuable)
+ def execute(content, issuable, only: nil)
return [content, {}] unless current_user.can?(:use_quick_actions)
@issuable = issuable
@updates = {}
- content, commands = extractor.extract_commands(content)
+ content, commands = extractor.extract_commands(content, only: only)
extract_updates(commands)
[content, @updates]
diff --git a/app/services/submodules/update_service.rb b/app/services/submodules/update_service.rb
new file mode 100644
index 00000000000..a6011a920bd
--- /dev/null
+++ b/app/services/submodules/update_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Submodules
+ class UpdateService < Commits::CreateService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(*args)
+ super
+
+ @start_branch = @branch_name
+ @commit_sha = params[:commit_sha].presence
+ @submodule = params[:submodule].presence
+ @commit_message = params[:commit_message].presence || "Update submodule #{@submodule} with oid #{@commit_sha}"
+ end
+
+ def validate!
+ super
+
+ raise ValidationError, 'The repository is empty' if repository.empty?
+ end
+
+ def execute
+ super
+ rescue StandardError => e
+ error(e.message)
+ end
+
+ def create_commit!
+ repository.update_submodule(current_user,
+ @submodule,
+ @commit_sha,
+ message: @commit_message,
+ branch: @branch_name)
+ rescue ArgumentError, TypeError
+ raise ValidationError, 'Invalid parameters'
+ end
+ end
+end
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 391115a67b5..84c3dfd8b91 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,6 +1,10 @@
-- page_title "Report abuse"
-%h3.page-title Report abuse
-%p Please use this form to report users who create spam issues, comments or behave inappropriately.
+- page_title _("Report abuse to GitLab")
+%h3.page-title
+ = _('Report abuse to GitLab')
+%p
+ = _('Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately.')
+%p
+ = _("A member of GitLab's abuse team will review your report as soon as possible.")
%hr
= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
@@ -16,7 +20,7 @@
.col-sm-10
= f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.form-text.text-muted
- Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
+ = _('Explain the problem. If appropriate, provide a link to the relevant issue or comment.')
.form-actions
= f.submit "Send report", class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 86339e61215..60a6be731ea 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -20,5 +20,11 @@
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
+ .form-group
+ = f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
+ = f.text_field :commit_email_hostname, class: 'form-control'
+ .form-text.text-muted
+ - commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank'
+ = _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml
new file mode 100644
index 00000000000..db2e247e341
--- /dev/null
+++ b/app/views/clusters/clusters/_buttons.html.haml
@@ -0,0 +1,4 @@
+-# This partial is overridden in EE
+.nav-controls
+ %span.btn.btn-add-cluster.disabled.js-add-cluster
+ = s_("ClusterIntegration|Add Kubernetes cluster")
diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
index facbcb7fc59..adeca013749 100644
--- a/app/views/clusters/clusters/_cluster.html.haml
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -1,24 +1,16 @@
-.gl-responsive-table-row
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
- .table-mobile-content
- = link_to cluster.name, cluster.show_path
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
- .table-mobile-content= cluster.environment_scope
- .table-section.section-30
- .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
- .table-mobile-content= cluster.platform_kubernetes&.actual_namespace
- .table-section.section-10
- .table-mobile-header{ role: "rowheader" }
- .table-mobile-content
- %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
- class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
- "aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
- disabled: !cluster.can_toggle_cluster?,
- data: { endpoint: clusterable.cluster_path(cluster, format: :json) } }
- %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
- = icon("spinner spin", class: "loading-icon")
- %span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+.card
+ .card-body.gl-responsive-table-row
+ .table-section.section-60
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
+ .table-mobile-content
+ = link_to cluster.name, cluster.show_path
+ - unless cluster.enabled?
+ %span.badge.badge-danger Connection disabled
+ .table-section.section-25
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-15.text-right
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %span.badge.badge-light
+ = cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
index 800e76d92ef..c926ec258f0 100644
--- a/app/views/clusters/clusters/_empty_state.html.haml
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -4,8 +4,10 @@
.col-12
.text-content
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
+ %p
+ = s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = clusterable.empty_state_help_text
+ = clusterable.learn_more_link
- if clusterable.can_create_cluster?
.text-center
diff --git a/app/views/clusters/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml
index 3d10348212f..6e4415c21a9 100644
--- a/app/views/clusters/clusters/_sidebar.html.haml
+++ b/app/views/clusters/clusters/_sidebar.html.haml
@@ -1,9 +1,6 @@
-- clusters_help_url = help_page_path('user/project/clusters/index.md')
-- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe
-- help_link_end = '</a>'.html_safe
%h4.prepend-top-0
- = s_('ClusterIntegration|Kubernetes cluster integration')
+ = s_('ClusterIntegration|Add a Kubernetes cluster integration')
%p
- = s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
+ = clusterable.sidebar_text
%p
- = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end }
+ = clusterable.learn_more_link
diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
index ca55ccb8fdf..e9f05eaf453 100644
--- a/app/views/clusters/clusters/gcp/_show.html.haml
+++ b/app/views/clusters/clusters/gcp/_show.html.haml
@@ -33,9 +33,10 @@
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index a55de84b5cd..ad6d1d856d6 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -10,14 +10,13 @@
.top-area.adjust
.nav-text
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
- .ci-table.js-clusters-list
+ = render 'clusters/clusters/buttons'
+ .clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
- .table-section.section-30{ role: "rowheader" }
+ .table-section.section-60{ role: "rowheader" }
= s_("ClusterIntegration|Kubernetes cluster")
.table-section.section-30{ role: "rowheader" }
= s_("ClusterIntegration|Environment scope")
- .table-section.section-30{ role: "rowheader" }
- = s_("ClusterIntegration|Project namespace")
.table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 1e1157c34bd..8a7f7a5c978 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -13,7 +13,9 @@
install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
+ install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
+ cluster_type: @cluster.cluster_type,
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index e4758938059..9793c77fc2b 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -21,9 +21,10 @@
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @user_cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
index ad8c35e32e3..cac8e72edd3 100644
--- a/app/views/clusters/clusters/user/_show.html.haml
+++ b/app/views/clusters/clusters/user/_show.html.haml
@@ -22,9 +22,10 @@
%button.js-show-cluster-token.btn-blank{ type: 'button' }
= s_('ClusterIntegration|Show')
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ - if @cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
+ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
.form-check
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index b11dc2c8e9b..6eb067da95c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -5,9 +5,4 @@
= render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.d-block.d-sm-none
- &nbsp;
- = link_to new_snippet_path, class: "btn btn-success btn-block", title: "New snippet" do
- New snippet
-
= render partial: 'snippets/snippets', locals: { link_project: true }
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 0904e44a658..51dcc9d0cda 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -35,7 +35,7 @@
%p
= _('Who will be able to see this group?')
= link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank'
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
+ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group, with_label: false
= render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index ae09e0dfa18..56d4f2ba881 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -84,4 +84,6 @@
= link_to 'import flow', status_import_bitbucket_server_path
again.
+= paginate_without_count(@collection)
+
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b7d69539eb7..474ef25cef7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -71,7 +71,7 @@
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
- %li.nav-item
+ %li.nav-item.m-auto
%div
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 163556f4509..3cd5168c1f7 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -74,6 +74,8 @@
%span
= boards_link_text
+ = render_if_exists 'layouts/nav/issues_analytics_link'
+
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
@@ -114,6 +116,19 @@
%strong.fly-out-top-item-name
= _('Members')
+ - if group_sidebar_link?(:kubernetes)
+ = nav_link(controller: [:clusters]) do
+ = link_to group_clusters_path(@group) do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Kubernetes')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
+ %strong.fly-out-top-item-name
+ = _('Kubernetes')
+
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 174033f3d49..ab15889a465 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -195,7 +195,7 @@
= _('Charts')
- if project_nav_tab? :operations
- = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
+ = nav_link(controller: sidebar_operations_paths) do
= link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
@@ -203,7 +203,7 @@
= _('Operations')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
@@ -215,6 +215,8 @@
%span
= _('Metrics')
+ = render_if_exists "layouts/nav/sidebar/tracing_link"
+
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
%span
@@ -326,6 +328,7 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
+ = render_if_exists 'projects/sidebar/settings_operations'
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index ea215e3e718..2603c558c0f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -91,8 +91,9 @@
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2'
- = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email),
- { help: 'This email will be used for web based operations, such as edits and merges.' },
+ - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank')
+ = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)),
+ { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } },
control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{ help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 88085c7185b..8de84f82e9f 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -11,8 +11,9 @@
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
- Report as abuse
+ = _('Report abuse to GitLab')
- if note_editable
%li
= link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do
- %span.text-danger Delete comment
+ %span.text-danger
+ = _('Delete comment')
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 41afaa9ffc0..621b7922072 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -91,7 +91,7 @@
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
- %code \d+\%\s*$
+ %code ^TOTAL\s+\d+\s+\d+\s+(\d+\%)$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 0d2f6bb77d6..f0d1dd162df 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -10,7 +10,7 @@
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
- %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
+ %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal, show_sorting_dropdown: false
%script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 9133ce8ed22..bee26cd8312 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -8,7 +8,7 @@
%p= _("You can also star a label to make it a priority label.")
.text-center
- if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success qa-label-create-new', title: _('New label'), id: 'new_label_link'
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
- if can?(current_user, :admin_label, @group)
= link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d27f79dc404..95f32bd0180 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -3,6 +3,7 @@
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
- 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 }
@@ -140,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 type != :boards_modal
+ - elsif show_sorting_dropdown
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 9dd1c24fdfa..ec1e10bb0c1 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -7,8 +7,8 @@
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline
- %li.timeline-entry
+ .notes.notes-form.timeline
+ .timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 12706613ac2..bf637f82df2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -40,6 +40,8 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
+ when Gitlab::Email::InvalidAttachment
+ error.message
when Gitlab::Email::InvalidRecordError
can_retry = true
error.message
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index de92f3eca6a..667a4121131 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -7,60 +7,58 @@ class StuckImportJobsWorker
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform
- projects_without_jid_count = mark_projects_without_jid_as_failed!
- projects_with_jid_count = mark_projects_with_jid_as_failed!
+ import_state_without_jid_count = mark_import_states_without_jid_as_failed!
+ import_state_with_jid_count = mark_import_states_with_jid_as_failed!
Gitlab::Metrics.add_event(:stuck_import_jobs,
- projects_without_jid_count: projects_without_jid_count,
- projects_with_jid_count: projects_with_jid_count)
+ projects_without_jid_count: import_state_without_jid_count,
+ projects_with_jid_count: import_state_with_jid_count)
end
private
- def mark_projects_without_jid_as_failed!
- enqueued_projects_without_jid.each do |project|
- project.mark_import_as_failed(error_message)
+ def mark_import_states_without_jid_as_failed!
+ enqueued_import_states_without_jid.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: disable CodeReuse/ActiveRecord
- def mark_projects_with_jid_as_failed!
- # TODO: Rollback this change to use SQL through #pluck
- jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
+ def mark_import_states_with_jid_as_failed!
+ jids_and_ids = enqueued_import_states_with_jid.pluck(:jid, :id).to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
return unless completed_jids.any?
- completed_project_ids = jids_and_ids.values_at(*completed_jids)
+ completed_import_state_ids = jids_and_ids.values_at(*completed_jids)
- # We select the projects again, because they may have transitioned from
+ # We select the import states again, because they may have transitioned from
# scheduled/started to finished/failed while we were looking up their Sidekiq status.
- completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids)
+ completed_import_states = enqueued_import_states_with_jid.where(id: completed_import_state_ids)
- Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}")
+ completed_import_state_jids = completed_import_states.map { |import_state| import_state.jid }.join(', ')
+ Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_import_state_jids}")
- completed_projects.each do |project|
- project.mark_import_as_failed(error_message)
+ completed_import_states.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects
- Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
+ def enqueued_import_states
+ ProjectImportState.with_status([:scheduled, :started])
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_with_jid
- enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_with_jid
+ enqueued_import_states.where.not(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_without_jid
- enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_without_jid
+ enqueued_import_states.where(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml b/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml
new file mode 100644
index 00000000000..7ac2410b18c
--- /dev/null
+++ b/changelogs/unreleased/21480-parallel-job-keyword-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Implement parallel job keyword.
+merge_request: 22631
+author:
+type: added
diff --git a/changelogs/unreleased/34758-group-cluster-controller.yml b/changelogs/unreleased/34758-group-cluster-controller.yml
new file mode 100644
index 00000000000..88c4c872714
--- /dev/null
+++ b/changelogs/unreleased/34758-group-cluster-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to create group level clusters and install gitlab managed applications
+merge_request: 22450
+author:
+type: added
diff --git a/changelogs/unreleased/43521-keep-personal-emails-private.yml b/changelogs/unreleased/43521-keep-personal-emails-private.yml
new file mode 100644
index 00000000000..0f0bede6482
--- /dev/null
+++ b/changelogs/unreleased/43521-keep-personal-emails-private.yml
@@ -0,0 +1,5 @@
+---
+title: Adds option to override commit email with a noreply private email
+merge_request: 22560
+author:
+type: added
diff --git a/changelogs/unreleased/47008-issue-board-card-design.yml b/changelogs/unreleased/47008-issue-board-card-design.yml
new file mode 100644
index 00000000000..39238687943
--- /dev/null
+++ b/changelogs/unreleased/47008-issue-board-card-design.yml
@@ -0,0 +1,5 @@
+---
+title: Issue board card design
+merge_request: 21229
+author:
+type: changed
diff --git a/changelogs/unreleased/51259-ci-cd-tooltips.yml b/changelogs/unreleased/51259-ci-cd-tooltips.yml
new file mode 100644
index 00000000000..fc0010dbeba
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-tooltips.yml
@@ -0,0 +1,6 @@
+---
+title: Replaces tooltip directive with the new gl-tooltip directive for consistency
+ in some ci/cd code
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/52767-more-chaos-for-gitlab.yml b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
new file mode 100644
index 00000000000..067777cb7fa
--- /dev/null
+++ b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoints for simulating certain failure modes in the application
+merge_request: 22746
+author:
+type: other
diff --git a/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml b/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml
new file mode 100644
index 00000000000..a05ef75b6a6
--- /dev/null
+++ b/changelogs/unreleased/52771-ldap-users-can-t-choose-private-or-internal-when-creating-a-new-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug stopping non-admin users from changing visibility level on group creation
+merge_request: 22468
+author:
+type: fixed
diff --git a/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml b/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml
new file mode 100644
index 00000000000..66301329c52
--- /dev/null
+++ b/changelogs/unreleased/53640-follow-up-from-resolve-redesign-activity-feed.yml
@@ -0,0 +1,4 @@
+title: Adds new icon size to Vue icon component
+merge_request: 22899
+author:
+type: other
diff --git a/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
new file mode 100644
index 00000000000..3e87c5875c6
--- /dev/null
+++ b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix misaligned approvers dropdown
+merge_request: 22832
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-action-to-deployment.yml b/changelogs/unreleased/add-action-to-deployment.yml
new file mode 100644
index 00000000000..4629f762ae8
--- /dev/null
+++ b/changelogs/unreleased/add-action-to-deployment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix environment status in merge request widget
+merge_request: 22799
+author:
+type: changed
diff --git a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
deleted file mode 100644
index b89ba754952..00000000000
--- a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make new merge request URL more friendly when pushing code
-merge_request: 22526
-author: "@blackst0ne"
-type: changed
diff --git a/changelogs/unreleased/bvl-patches-via-mail.yml b/changelogs/unreleased/bvl-patches-via-mail.yml
new file mode 100644
index 00000000000..6fd9e6a956c
--- /dev/null
+++ b/changelogs/unreleased/bvl-patches-via-mail.yml
@@ -0,0 +1,5 @@
+---
+title: Allow adding patches when creating a merge request via email
+merge_request: 22723
+author: Serdar Dogruyol
+type: added
diff --git a/changelogs/unreleased/diff-expand-all-button.yml b/changelogs/unreleased/diff-expand-all-button.yml
new file mode 100644
index 00000000000..77600e726d5
--- /dev/null
+++ b/changelogs/unreleased/diff-expand-all-button.yml
@@ -0,0 +1,5 @@
+---
+title: Show expand all diffs button when a single diff file is collapsed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml
new file mode 100644
index 00000000000..8e02a9019df
--- /dev/null
+++ b/changelogs/unreleased/dm-api-merge-requests-index-merged-at.yml
@@ -0,0 +1,5 @@
+---
+title: Expose {closed,merged}_{at,by} in merge requests API index
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
new file mode 100644
index 00000000000..d8455a8509f
--- /dev/null
+++ b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Improves performance of stuck import jobs detection
+merge_request: 22879
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
new file mode 100644
index 00000000000..c06b02b05e8
--- /dev/null
+++ b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoint to update a git submodule reference
+merge_request: 20949
+author:
+type: added
diff --git a/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml b/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml
new file mode 100644
index 00000000000..5add6d727ac
--- /dev/null
+++ b/changelogs/unreleased/fj-50890-fix-commit-message-wiki-new-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug with wiki page create message
+merge_request: 22849
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml b/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml
new file mode 100644
index 00000000000..9d44e46c0ed
--- /dev/null
+++ b/changelogs/unreleased/fj-bump-gitaly-0-129-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.129.0
+merge_request: 22868
+author:
+type: added
diff --git a/changelogs/unreleased/gl-ui-loading-icon.yml b/changelogs/unreleased/gl-ui-loading-icon.yml
new file mode 100644
index 00000000000..5540fc7d7ea
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's loading icon from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gt-align-sign-in-button.yml b/changelogs/unreleased/gt-align-sign-in-button.yml
new file mode 100644
index 00000000000..a51fa319231
--- /dev/null
+++ b/changelogs/unreleased/gt-align-sign-in-button.yml
@@ -0,0 +1,5 @@
+---
+title: Align sign in button
+merge_request: 22888
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml b/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml
new file mode 100644
index 00000000000..51af2807a03
--- /dev/null
+++ b/changelogs/unreleased/gt-use-merge-request-prefix-in-event-feed-title.yml
@@ -0,0 +1,5 @@
+---
+title: Use merge request prefix symbol in event feed title
+merge_request: 22449
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/introduce-knative-support.yml b/changelogs/unreleased/introduce-knative-support.yml
new file mode 100644
index 00000000000..53290d71977
--- /dev/null
+++ b/changelogs/unreleased/introduce-knative-support.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Knative support
+author: Chris Baumbauer
+merge_request: 43959
+type: added
diff --git a/changelogs/unreleased/max_retries_when.yml b/changelogs/unreleased/max_retries_when.yml
new file mode 100644
index 00000000000..dad3cd8a123
--- /dev/null
+++ b/changelogs/unreleased/max_retries_when.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to configure when to retry failed CI jobs
+merge_request: 21758
+author: Markus Doits
+type: added
diff --git a/changelogs/unreleased/mr-image-commenting.yml b/changelogs/unreleased/mr-image-commenting.yml
new file mode 100644
index 00000000000..3cc3becc795
--- /dev/null
+++ b/changelogs/unreleased/mr-image-commenting.yml
@@ -0,0 +1,5 @@
+---
+title: Reimplemented image commenting in merge request diffs
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/refactor-snippets-finder.yml b/changelogs/unreleased/refactor-snippets-finder.yml
new file mode 100644
index 00000000000..37cacf71c14
--- /dev/null
+++ b/changelogs/unreleased/refactor-snippets-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Rewrite SnippetsFinder to improve performance by a factor of 1500
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/remove-asset-sync.yml b/changelogs/unreleased/remove-asset-sync.yml
new file mode 100644
index 00000000000..ddb82212975
--- /dev/null
+++ b/changelogs/unreleased/remove-asset-sync.yml
@@ -0,0 +1,5 @@
+---
+title: Remove asset_sync gem from Gemfile and related code from codebase
+merge_request: 22610
+author:
+type: other
diff --git a/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml b/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml
new file mode 100644
index 00000000000..3a8b3a0df5d
--- /dev/null
+++ b/changelogs/unreleased/remove-duplicate-primary-button-in-dashboard-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicate primary button in dashboard snippets on small viewports
+merge_request: 22902
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/rs-revert-api.yml b/changelogs/unreleased/rs-revert-api.yml
new file mode 100644
index 00000000000..c07b2fe624c
--- /dev/null
+++ b/changelogs/unreleased/rs-revert-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add revert to commits API
+merge_request: 22919
+author:
+type: added
diff --git a/changelogs/unreleased/sh-fix-issue-52649.yml b/changelogs/unreleased/sh-fix-issue-52649.yml
new file mode 100644
index 00000000000..34b7f74a345
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-52649.yml
@@ -0,0 +1,5 @@
+---
+title: Fix statement timeouts in RemoveRestrictedTodos migration
+merge_request: 22795
+author:
+type: other
diff --git a/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
new file mode 100644
index 00000000000..b5743e71cf9
--- /dev/null
+++ b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
@@ -0,0 +1,5 @@
+---
+title: Paginate Bitbucket Server importer projects
+merge_request: 22825
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-index-uploads-file-store.yml b/changelogs/unreleased/tc-index-uploads-file-store.yml
new file mode 100644
index 00000000000..fa3b3164e38
--- /dev/null
+++ b/changelogs/unreleased/tc-index-uploads-file-store.yml
@@ -0,0 +1,5 @@
+---
+title: Enhance performance of counting local Uploads
+merge_request: 22522
+author:
+type: performance
diff --git a/changelogs/unreleased/upgrade-workhorse-7-1-0.yml b/changelogs/unreleased/upgrade-workhorse-7-1-0.yml
new file mode 100644
index 00000000000..b6df35e6d10
--- /dev/null
+++ b/changelogs/unreleased/upgrade-workhorse-7-1-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab-Workhorse to v7.1.0
+merge_request: 22883
+author:
+type: other
diff --git a/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml b/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml
new file mode 100644
index 00000000000..fbedd2796b2
--- /dev/null
+++ b/changelogs/unreleased/winh-delayed-jobs-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer to delayed jobs
+merge_request: 22382
+author:
+type: changed
diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb
deleted file mode 100644
index 7f3934853fa..00000000000
--- a/config/initializers/asset_sync.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-AssetSync.configure do |config|
- # Disable the asset_sync gem by default. If it is enabled, but not configured,
- # asset_sync will cause the build to fail.
- config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED')
- ENV['ASSET_SYNC_ENABLED'] == 'true'
- else
- false
- end
-
- # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40
- # This allows us to disable asset_sync by default and configure through environment variables
- # Updates to asset_sync gem should be checked
- config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER')
- config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
- config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
-
- config.aws_access_key_id = ENV['ASSETS_AWS_ACCESS_KEY_ID'] if ENV.has_key?('ASSETS_AWS_ACCESS_KEY_ID')
- config.aws_secret_access_key = ENV['ASSETS_AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('ASSETS_AWS_SECRET_ACCESS_KEY')
- config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
-
- config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
- config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY')
-
- config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID')
- config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY')
-
- config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
-
- config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
- config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
-end
diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb
index 0f45cf44621..18e067c8854 100644
--- a/config/initializers/fill_shards.rb
+++ b/config/initializers/fill_shards.rb
@@ -1,4 +1,3 @@
-return unless Shard.connected?
-return if Gitlab::Database.read_only?
-
-Shard.populate!
+if Shard.connected? && !Gitlab::Database.read_only?
+ Shard.populate!
+end
diff --git a/config/routes.rb b/config/routes.rb
index d2d91647d0b..484e05114be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -82,6 +82,13 @@ Rails.application.routes.draw do
draw :operations
draw :instance_statistics
+
+ if ENV['GITLAB_ENABLE_CHAOS_ENDPOINTS']
+ get '/chaos/leakmem' => 'chaos#leakmem'
+ get '/chaos/cpuspin' => 'chaos#cpuspin'
+ get '/chaos/sleep' => 'chaos#sleep'
+ get '/chaos/kill' => 'chaos#kill'
+ end
end
concern :clusterable do
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 2328b50b760..a0aeebe4b91 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -53,6 +53,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :avatar, only: [:destroy]
+ concerns :clusterable
+
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 387d2363552..3f1ad90dfca 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -149,7 +149,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope path: 'merge_requests', controller: 'merge_requests/creations' do
post '', action: :create, as: nil
- scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do
+ scope path: 'new', as: :new_merge_request do
+ get '', action: :new
+
scope constraints: { format: nil }, action: :new do
get :diffs, defaults: { tab: 'diffs' }
get :pipelines, defaults: { tab: 'pipelines' }
@@ -163,7 +165,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :diff_for_path
get :branch_from
get :branch_to
- get '', action: :new
end
end
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
index 51fc9e6bfca..1adca152736 100644
--- a/danger/metadata/Dangerfile
+++ b/danger/metadata/Dangerfile
@@ -23,3 +23,10 @@ has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('
if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
warn "Most of the time, merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
end
+
+if gitlab.mr_json['title'].length > 72
+ warn 'The title of this merge request is longer than 72 characters and ' \
+ 'would violate our commit message rules when using the Squash on Merge ' \
+ 'feature. Please consider adjusting the title, or rebase the ' \
+ "commits manually and don't use Squash on Merge."
+end
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 0b32a461d56..16243b72f9a 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -8,7 +8,8 @@ Gitlab::Seeder.quiet do
description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample,
milestone: project.milestones.sample,
- assignees: [project.team.users.sample]
+ assignees: [project.team.users.sample],
+ created_at: rand(12).months.ago
}
Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb
new file mode 100644
index 00000000000..bfda6a945a7
--- /dev/null
+++ b/db/migrate/20180912111628_add_knative_application.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddKnativeApplication < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table "clusters_applications_knative" do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+ end
+end
diff --git a/db/migrate/20181005125926_add_index_to_uploads_store.rb b/db/migrate/20181005125926_add_index_to_uploads_store.rb
new file mode 100644
index 00000000000..d32ca05e980
--- /dev/null
+++ b/db/migrate/20181005125926_add_index_to_uploads_store.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToUploadsStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :uploads, :store
+ end
+
+ def down
+ remove_concurrent_index :uploads, :store
+ end
+end
diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
new file mode 100644
index 00000000000..89ddaf2ae2b
--- /dev/null
+++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:application_settings, :commit_email_hostname, :string, null: true)
+ end
+end
diff --git a/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb
new file mode 100644
index 00000000000..ede0ee27b8a
--- /dev/null
+++ b/db/migrate/20181026143227_migrate_snippets_access_level_default_value.rb
@@ -0,0 +1,42 @@
+# 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 MigrateSnippetsAccessLevelDefaultValue < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ ENABLED = 20
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ change_column_default :project_features, :snippets_access_level, ENABLED
+
+ # On GitLab.com this will update about 28 000 rows. Since our updates are
+ # very small and this column is not indexed, these updates should be very
+ # lightweight.
+ ProjectFeature.where(snippets_access_level: nil).each_batch do |batch|
+ batch.update_all(snippets_access_level: ENABLED)
+ end
+
+ # We do not need to perform this in a post-deployment migration as the
+ # ProjectFeature model already enforces a default value for all new rows.
+ change_column_null :project_features, :snippets_access_level, false
+ end
+
+ def down
+ change_column_null :project_features, :snippets_access_level, true
+ change_column_default :project_features, :snippets_access_level, nil
+
+ # We can't migrate from 20 -> NULL, as some projects may have explicitly set
+ # the access level to 20.
+ end
+end
diff --git a/db/migrate/20181106135939_add_index_to_deployments.rb b/db/migrate/20181106135939_add_index_to_deployments.rb
new file mode 100644
index 00000000000..5f988a4723c
--- /dev/null
+++ b/db/migrate/20181106135939_add_index_to_deployments.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:project_id, :status, :created_at]
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:project_id, :status, :created_at]
+ end
+end
diff --git a/db/post_migrate/20181105201455_steal_fill_store_upload.rb b/db/post_migrate/20181105201455_steal_fill_store_upload.rb
new file mode 100644
index 00000000000..982001fedbe
--- /dev/null
+++ b/db/post_migrate/20181105201455_steal_fill_store_upload.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class StealFillStoreUpload < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+
+ disable_ddl_transaction!
+
+ class Upload < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled # Disable STI
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('FillStoreUpload')
+
+ Upload.where(store: nil).each_batch(of: BATCH_SIZE) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::FillStoreUpload.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20181107054254_remove_restricted_todos_again.rb b/db/post_migrate/20181107054254_remove_restricted_todos_again.rb
new file mode 100644
index 00000000000..644e0074c46
--- /dev/null
+++ b/db/post_migrate/20181107054254_remove_restricted_todos_again.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# rescheduling of the revised RemoveRestrictedTodosWithCte background migration
+class RemoveRestrictedTodosAgain < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ MIGRATION = 'RemoveRestrictedTodos'.freeze
+ BATCH_SIZE = 1000
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ Project.where('EXISTS (SELECT 1 FROM todos WHERE todos.project_id = projects.id)')
+ .each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, range)
+ end
+ end
+
+ def down
+ # nothing to do
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fe30c5375f4..56137caf1d7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20181101144347) do
+ActiveRecord::Schema.define(version: 20181107054254) 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: 20181101144347) do
t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
+ t.string "commit_email_hostname"
end
create_table "audit_events", force: :cascade do |t|
@@ -704,6 +705,16 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.text "status_reason"
end
+ create_table "clusters_applications_knative", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
@@ -836,6 +847,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_index "deployments", ["environment_id", "status"], name: "index_deployments_on_environment_id_and_status", using: :btree
add_index "deployments", ["id"], name: "partial_index_deployments_for_legacy_successful_deployments", where: "((finished_at IS NULL) AND (status = 2))", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
+ add_index "deployments", ["project_id", "status", "created_at"], name: "index_deployments_on_project_id_and_status_and_created_at", using: :btree
add_index "deployments", ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree
create_table "emails", force: :cascade do |t|
@@ -1631,7 +1643,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
t.integer "wiki_access_level"
- t.integer "snippets_access_level"
+ t.integer "snippets_access_level", default: 20, null: false
t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
@@ -2155,6 +2167,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
+ add_index "uploads", ["store"], name: "index_uploads_on_store", using: :btree
add_index "uploads", ["uploader", "path"], name: "index_uploads_on_uploader_and_path", using: :btree
create_table "user_agent_details", force: :cascade do |t|
@@ -2419,6 +2432,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify
+ add_foreign_key "clusters_applications_knative", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
index 664657650d4..ae38094391b 100644
--- a/doc/administration/auth/okta.md
+++ b/doc/administration/auth/okta.md
@@ -11,7 +11,7 @@ The following documentation enables Okta as a SAML provider.
1. When the app screen comes up you see another button to **Create an App** and
choose SAML 2.0 on the next screen.
1. Now, very important, add a logo
- (you can choose it from https://about.gitlab.com/press/). You'll have to
+ (you can choose it from <https://about.gitlab.com/press/>). You'll have to
crop and resize it.
1. Next, you'll need the to fill in the SAML general config. Here's an example
image.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 890b780fe80..cfe7b0e05e3 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -71,7 +71,7 @@ A Registry init file is not shipped with GitLab if you install it from source.
Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
you modify its settings. Read the upstream documentation on how to achieve that.
-At the absolute minimum, make sure your [Registry configuration][registry-auth]
+At the **absolute** minimum, make sure your [Registry configuration][registry-auth]
has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
as the realm:
@@ -84,6 +84,9 @@ auth:
rootcertbundle: /root/certs/certbundle
```
+CAUTION: **Caution:**
+If `auth` is not set up, users will be able to pull docker images without authentication.
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index c58ced7d520..60ad4bf4e8f 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -60,7 +60,7 @@ installations, this can be set in `gitlab-shell/config.yml`.
The hooks are searched and executed in this order:
-1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
+1. `gitlab-shell/hooks` directory as known to Gitaly
1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c6fd7ef7360..c9a2778b3a4 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -45,6 +45,7 @@ The following metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
+| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
diff --git a/doc/api/README.md b/doc/api/README.md
index a620a13a3b3..19abbdc7a1e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,6 +61,7 @@ following locations:
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
+- [Repository Submodules](repository_submodules.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
@@ -234,7 +235,7 @@ provided you are authenticated as an administrator with an OAuth or Personal Acc
You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `Sudo`.
+header name must be `Sudo`.
NOTE: **Note:**
Usernames are case insensitive.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9b7ca4b6e70..994eefa423f 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -288,6 +288,47 @@ Example response:
}
```
+## Revert a commit
+
+> [Introduced][ce-22919] in GitLab 11.6.
+
+Reverts a commit in a given branch.
+
+```
+POST /projects/:id/repository/commits/:sha/revert
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `sha` | string | yes | Commit SHA to revert |
+| `branch` | string | yes | Target branch name |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/a738f717824ff53aebad8b090c1b79a14f2bd9e8/revert"
+```
+
+Example response:
+
+```json
+{
+ "id":"8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+ "short_id": "8b090c1b",
+ "title":"Revert \"Feature added\"",
+ "created_at":"2018-11-08T15:55:26.000Z",
+ "parent_ids":["a738f717824ff53aebad8b090c1b79a14f2bd9e8"],
+ "message":"Revert \"Feature added\"\n\nThis reverts commit a738f717824ff53aebad8b090c1b79a14f2bd9e8",
+ "author_name":"Administrator",
+ "author_email":"admin@example.com",
+ "authored_date":"2018-11-08T15:55:26.000Z",
+ "committer_name":"Administrator",
+ "committer_email":"admin@example.com",
+ "committed_date":"2018-11-08T15:55:26.000Z"
+}
+```
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -619,3 +660,4 @@ Example response:
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
[ce-18004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18004
+[ce-22919]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22919
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index f3cfe0ad218..9cb3f0d9c0c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -57,7 +57,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -180,7 +191,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -293,7 +315,18 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
+ "merged_by": {
+ "id": 87854,
+ "name": "Douwe Maan",
+ "username": "DouweM",
+ "state": "active",
+ "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
+ "web_url": "https://gitlab.com/DouweM"
+ },
+ "merged_at": "2018-09-07T11:16:17.520Z",
+ "closed_by": null,
+ "closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -383,7 +416,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -695,7 +728,7 @@ POST /projects/:id/merge_requests
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -822,7 +855,7 @@ Must include at least one non-required attribute from above.
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -965,7 +998,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1080,7 +1113,7 @@ Parameters:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1279,7 +1312,7 @@ Example response:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1400,7 +1433,7 @@ Example response:
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
- "state": "opened",
+ "state": "merged",
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "master",
@@ -1540,7 +1573,7 @@ Example response:
"project_id": 3,
"title": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
"description": "Veniam sunt nihil modi earum cumque illum delectus. Nihil ad quis distinctio quia. Autem eligendi at quibusdam repellendus.",
- "state": "opened",
+ "state": "merged",
"created_at": "2016-06-17T07:48:04.330Z",
"updated_at": "2016-07-01T11:14:15.537Z",
"target_branch": "allow_regex_for_project_skip_ref",
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 5dbf6cb0760..c8de7f2191d 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -17,6 +17,7 @@ Parameters:
- `path` (optional) - The path inside repository. Used to get content of subdirectories
- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
+- `per_page` (optional) - Number of results to show per page. If not specified, defaults to `20`
```json
[
diff --git a/doc/api/repository_submodules.md b/doc/api/repository_submodules.md
new file mode 100644
index 00000000000..2e6797f18f4
--- /dev/null
+++ b/doc/api/repository_submodules.md
@@ -0,0 +1,49 @@
+# Repository submodules API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41213) in GitLab 11.5
+
+## Update existing submodule reference in repository
+
+In some workflows, especially automated ones, it can be useful to update a
+submodule's reference to keep up to date other projects that use it.
+This endpoint allows you to update a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) reference in a
+specific branch.
+
+```
+PUT /projects/:id/repository/submodules/:submodule
+```
+
+| 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 |
+| `submodule` | string | yes | URL encoded full path to the submodule. For example, `lib%2Fclass%2Erb` |
+| `branch` | string | yes | Name of the branch to commit into |
+| `commit_sha` | string | yes | Full commit SHA to update the submodule to |
+| `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"
+--data "branch=master&commit_sha=3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88&commit_message=Update submodule reference"
+```
+
+Example response:
+
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2018-09-20T09:26:24.000-07:00",
+ "message": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2018-09-20T09:26:24.000-07:00",
+ "authored_date": "2018-09-20T09:26:24.000-07:00",
+ "status": null
+}
+```
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 2d23bf6d2fd..bdbcf8c9435 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -65,6 +65,8 @@ future GitLab releases.**
| **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 |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 981aa101dd3..5deeb2b0133 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -74,7 +74,8 @@ A job is defined by a list of parameters that define the job behavior.
| after_script | no | Override a set of commands that are executed after job |
| environment | no | Defines a name of environment to which deployment is done by this job |
| coverage | no | Define code coverage settings for a given job |
-| retry | no | Define how many times a job can be auto-retried in case of a failure |
+| retry | no | Define when and how many times a job can be auto-retried in case of a failure |
+| parallel | no | Defines how many instances of a job should be run in parallel |
### `extends`
@@ -811,7 +812,7 @@ deploy to production:
> defined, GitLab will automatically trigger a stop action when the associated
> branch is deleted.
-Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
+Closing (stopping) environments can be achieved with the `on_stop` keyword defined under
`environment`. It declares a different job that runs in order to close
the environment.
@@ -1432,18 +1433,20 @@ job1:
## `retry`
> [Introduced][ce-12909] in GitLab 9.5.
+> [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21758)
+> in GitLab 11.5 to control on which failures to retry.
`retry` allows you to configure how many times a job is going to be retried in
case of a failure.
-When a job fails, and has `retry` configured it is going to be processed again
+When a job fails and has `retry` configured, it is going to be processed again
up to the amount of times specified by the `retry` keyword.
If `retry` is set to 2, and a job succeeds in a second run (first retry), it won't be retried
again. `retry` value has to be a positive integer, equal or larger than 0, but
lower or equal to 2 (two retries maximum, three runs in total).
-A simple example:
+A simple example to retry in all failure cases:
```yaml
test:
@@ -1451,6 +1454,77 @@ test:
retry: 2
```
+By default, a job will be retried on all failure cases. To have a better control
+on which failures to retry, `retry` can be a hash with with the following keys:
+
+- `max`: The maximum number of retries.
+- `when`: The failure cases to retry.
+
+To retry only runner system failures at maximum two times:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when: runner_system_failure
+```
+
+If there is another failure, other than a runner system failure, the job will
+not be retried.
+
+To retry on multiple failure cases, `when` can also be an array of failures:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when:
+ - runner_system_failure
+ - stuck_or_timeout_failure
+```
+
+Possible values for `when` are:
+
+<!--
+ Please make sure to update `RETRY_WHEN_IN_DOCUMENTATION` array in
+ `spec/lib/gitlab/ci/config/entry/retry_spec.rb` if you change any of
+ the documented values below. The test there makes sure that all documented
+ values are really valid as a config option and therefore should always
+ stay in sync with this documentation.
+ -->
+
+- `always`: Retry on any failure (default).
+- `unknown_failure`: Retry when the failure reason is unknown.
+- `script_failure`: Retry when the script failed.
+- `api_failure`: Retry on API failure.
+- `stuck_or_timeout_failure`: Retry when the job got stuck or timed out.
+- `runner_system_failure`: Retry if there was a runner system failure (e.g. setting up the job failed).
+- `missing_dependency_failure`: Retry if a dependency was missing.
+- `runner_unsupported`: Retry if the runner was unsupported.
+
+
+## `parallel`
+
+> [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.
+
+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`.
+
+For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-variables-environment-variables) are set.
+
+A simple example:
+
+```yaml
+test:
+ script: rspec
+ parallel: 5
+```
+
## `include`
> Introduced in [GitLab Edition Premium][ee] 10.5.
@@ -2034,4 +2108,4 @@ CI with various languages.
[schedules]: ../../user/project/pipelines/schedules.md
[variables-expressions]: ../variables/README.md#variables-expressions
[ee]: https://about.gitlab.com/gitlab-ee/
-[gitlab-versions]: https://about.gitlab.com/products/ \ No newline at end of file
+[gitlab-versions]: https://about.gitlab.com/products/
diff --git a/doc/development/chaos_endpoints.md b/doc/development/chaos_endpoints.md
new file mode 100644
index 00000000000..403a5b21827
--- /dev/null
+++ b/doc/development/chaos_endpoints.md
@@ -0,0 +1,117 @@
+# Generating chaos in a test GitLab instance
+
+As [Werner Vogels](https://twitter.com/Werner), the CTO at Amazon Web Services, famously put it, **Everything fails, all the time**.
+
+As a developer, it's as important to consider the failure modes in which your software will operate as much as normal operation. Doing so can mean the difference between a minor hiccup leading to a scattering of `500` errors experienced by a tiny fraction of users and a full site outage that affects all users for an extended period.
+
+To paraphrase [Tolstoy](https://en.wikipedia.org/wiki/Anna_Karenina_principle), _all happy servers are alike, but all failing servers are failing in their own way_. Luckily, there are ways we can attempt to simulate these failure modes, and the chaos endpoints are tools for assisting in this process.
+
+Currently, there are four endpoints for simulating the following conditions:
+
+- Slow requests.
+- CPU-bound requests.
+- Memory leaks.
+- Unexpected process crashes.
+
+## Enabling chaos endpoints
+
+For obvious reasons, these endpoints are not enabled by default. They can be enabled by setting the `GITLAB_ENABLE_CHAOS_ENDPOINTS` environment variable to `1`.
+
+For example, if you're using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 gdk run
+```
+
+## Securing the chaos endpoints
+
+DANGER: **Danger:**
+It is highly recommended that you secure access to the chaos endpoints using a secret token. This is recommended when enabling these endpoints locally and essential when running in a staging or other shared environment. You should not enable them in production unless you absolutely know what you're doing.
+
+A secret token can be set through the `GITLAB_CHAOS_SECRET` environment variable. For example, when using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 GITLAB_CHAOS_SECRET=secret gdk run
+```
+
+Replace `secret` with your own secret token.
+
+## Invoking chaos
+
+Once you have enabled the chaos endpoints and restarted the application, you can start testing using the endpoints.
+
+## Memory leaks
+
+To simulate a memory leak in your application, use the `/-/chaos/leakmem` endpoint.
+
+NOTE: **Note:**
+The memory is not retained after the request finishes. Once the request has completed, the Ruby garbage collector will attempt to recover the memory.
+
+```
+GET /-/chaos/leakmem
+GET /-/chaos/leakmem?memory_mb=1024
+GET /-/chaos/leakmem?memory_mb=1024&duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------------------- |
+| `memory_mb` | integer | no | How much memory, in MB, should be leaked. Defaults to 100MB. |
+| `duration_s` | integer | no | Minimum duration, in seconds, that the memory should be retained. Defaults to 30s. |
+
+```bash
+curl http://localhost:3000/-/chaos/leakmem?memory_mb=1024&duration_s=10 --header 'X-Chaos-Secret: secret'
+```
+
+## CPU spin
+
+This endpoint attempts to fully utilise a single core, at 100%, for the given period.
+
+Depending on your rack server setup, your request may timeout after a predermined period (normally 60 seconds).
+If you're using Unicorn, this is done by killing the worker process.
+
+```
+GET /-/chaos/cpuspin
+GET /-/chaos/cpuspin?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | --------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the core will be utilised. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/cpuspin?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Sleep
+
+This endpoint is similar to the CPU Spin endpoint but simulates off-processor activity, such as network calls to backend services. It will sleep for a given duration.
+
+As with the CPU Spin endpoint, this may lead to your request timing out if duration exceeds the configured limit.
+
+```
+GET /-/chaos/sleep
+GET /-/chaos/sleep?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the request will sleep for. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/sleep?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Kill
+
+This endpoint will simulate the unexpected death of a worker process using a `kill` signal.
+
+NOTE: **Note:**
+Since this endpoint uses the `KILL` signal, the worker is not given a chance to cleanup or shutdown.
+
+```
+GET /-/chaos/kill
+```
+
+```bash
+curl http://localhost:3000/-/chaos/kill --header 'X-Chaos-Secret: secret'
+```
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 4661d11b29e..233dc83f95b 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -8,7 +8,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
-- Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
+- Team: ~Plan, ~Manage, ~Quality, etc.
- Stage: ~"devops:plan", ~"devops:create", etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
@@ -61,7 +61,6 @@ people.
The current team labels are:
- ~Configure
-- ~"CI/CD"
- ~Create
- ~Distribution
- ~Documentation
@@ -74,6 +73,7 @@ The current team labels are:
- ~Release
- ~Secure
- ~UX
+- ~Verify
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 51f5ddfc1e0..154ede087cc 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -4,9 +4,9 @@ description: Learn how to contribute to GitLab Documentation.
# GitLab Documentation guidelines
- - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
+- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
+- **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Contributing to docs
@@ -41,7 +41,7 @@ how to structure GitLab docs.
## Markdown and styles
-Currently GitLab docs use Redcarpet as [markdown](../../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+Currently GitLab docs use [Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) as the [markdown](../../user/markdown.md) engine.
All the docs follow the [documentation style guidelines](styleguide.md). See [Linting](#linting) for help to follow the guidelines.
@@ -61,11 +61,11 @@ in small iterations. Please don't create new docs in these folders.
### Documentation files
- When you create a new directory, always start with an `index.md` file.
-Do not use another file name and **do not** create `README.md` files
+ Do not use another file name and **do not** create `README.md` files.
- **Do not** use special chars and spaces, or capital letters in file names,
-directory names, branch names, and anything that generates a path.
-- Max screenshot size: 100KB
-- We do not support videos (yet)
+ directory names, branch names, and anything that generates a path.
+- Max screenshot size: 100KB.
+- We do not support videos (yet).
### Location and naming documents
@@ -83,16 +83,16 @@ and cross-link between any related content.
The table below shows what kind of documentation goes where.
-| Directory | What belongs here |
-| --------- | -------------- |
-| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
-| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
-| `doc/api/` | API related documentation. |
-| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
-| `doc/legal/` | Legal documents about contributing to GitLab. |
-| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
-| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
-| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
+| Directory | What belongs here |
+|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
+| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/` | Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
+| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
---
@@ -134,13 +134,13 @@ merge request.
Changing a document's location is not to be taken lightly. Remember that the
documentation is available to all installations under `help/` and not only to
-GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
+GitLab.com or <http://docs.gitlab.com>. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather replace all of its contents with a new line:
-```
+```md
This document was moved to [another location](path/to/new_doc.md).
```
@@ -154,7 +154,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
- ```
+ ```md
This document was moved to [another location](../../administration/lfs.md).
```
@@ -162,7 +162,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab-ce` repository and then do:
- ```
+ ```sh
git grep -n "workflow/lfs/lfs_administration"
git grep -n "lfs/lfs_administration"
```
@@ -226,17 +226,16 @@ even if it's `index.html` or `README.html`.
## Linting
To help adhere to the [documentation style guidelines](styleguide.md), and to improve the content
- added to documentation, consider locally installing and running documentation linters. This will
- help you catch common issues before raising merge requests for review of documentation.
+added to documentation, consider locally installing and running documentation linters. This will
+help you catch common issues before raising merge requests for review of documentation.
The following are some suggested linters you can install locally and sample configuration:
-- `proselint`
-- `markdownlint`
+- [`proselint`](#proselint)
+- [`markdownlint`](#markdownlint)
NOTE: **Note:**
-This list does not limit what other linters you can add to your local documentation writing
- toolchain.
+This list does not limit what other linters you can add to your local documentation writing toolchain.
### `proselint`
@@ -262,19 +261,20 @@ proselint **/*.md
#### Sample `proselint` configuration
-All of the checks are good to use. However, excluding the `typography.symbols` checks might reduce
- noise. The following sample `proselint` configuration disables the `typography.symbols` checks:
+All of the checks are good to use. However, excluding the `typography.symbols` and `misc.phrasal_adjectives` checks will reduce
+noise. The following sample `proselint` configuration disables these checks:
```json
{
"checks": {
- "typography.symbols": false
+ "typography.symbols": false,
+ "misc.phrasal_adjectives": false
}
}
```
A file with `proselint` configuration must be placed in a
- [valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
+[valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
### `markdownlint`
@@ -306,6 +306,7 @@ The following sample `markdownlint` configuration modifies the available default
- Adhere to the [style guidelines](styleguide.md).
- Apply conventions found in the GitLab documentation.
+- Allow the flexibility of using some inline HTML.
```json
{
@@ -316,14 +317,31 @@ The following sample `markdownlint` configuration modifies the available default
"no-trailing-punctuation": false,
"ol-prefix": { "style": "one" },
"blanks-around-fences": false,
+ "no-inline-html": {
+ "allowed_elements": [
+ "table",
+ "tbody",
+ "tr",
+ "td",
+ "ul",
+ "ol",
+ "li",
+ "br",
+ "img",
+ "a",
+ "strong",
+ "i",
+ "div"
+ ]
+ },
"hr-style": { "style": "---" },
"fenced-code-language": false
}
```
For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be
- placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
- example, `~/.markdownlintrc`.
+placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
+example, `~/.markdownlintrc`.
## Testing
@@ -350,8 +368,8 @@ If your contribution contains **only** documentation changes, you can speed up
the CI process by following some branch naming conventions. You have three
choices:
-| Branch name | Valid example |
-| ----------- | ------------- |
+| Branch name | Valid example |
+|:----------------------|:-----------------------------|
| Starting with `docs/` | `docs/update-api-issues` |
| Starting with `docs-` | `docs-update-api-issues` |
| Ending in `-docs` | `123-update-api-issues-docs` |
@@ -400,15 +418,15 @@ Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.m
- Create the EE-equivalent branch ending with `-ee`, e.g.,
`git checkout -b docs-example-ee`
- Once all the jobs are passing in CE and EE, and you've addressed the
-feedback from your own team, assign the CE MR to a technical writer for review
+ feedback from your own team, assign the CE MR to a technical writer for review
- When both MRs are ready, the EE merge request will be merged first, and the
-CE-equivalent will be merged next.
+ CE-equivalent will be merged next.
- Note that the review will occur only in the CE MR, as the EE MR
-contains the same commits as the CE MR.
+ contains the same commits as the CE MR.
- If you have a few more changes that apply to the EE-version only, you can submit
-a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
-additionally to the CE MR. If there are many EE-only changes though, start a new MR
-to EE only.
+ a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
+ additionally to the CE MR. If there are many EE-only changes though, start a new MR
+ to EE only.
## Previewing the changes live
@@ -418,9 +436,9 @@ To preview your changes to documentation locally, follow this
The live preview is currently enabled for the following projects:
-- https://gitlab.com/gitlab-org/gitlab-ce
-- https://gitlab.com/gitlab-org/gitlab-ee
-- https://gitlab.com/gitlab-org/gitlab-runner
+- <https://gitlab.com/gitlab-org/gitlab-ce>
+- <https://gitlab.com/gitlab-org/gitlab-ee>
+- <https://gitlab.com/gitlab-org/gitlab-runner>
If your branch contains only documentation changes, you can use
[special branch names](#branch-naming) to avoid long running pipelines.
@@ -516,8 +534,8 @@ Every GitLab instance includes the documentation, which is available from `/help
The documentation available online on docs.gitlab.com is continuously
deployed every hour from the `master` branch of CE, EE, Omnibus, and Runner. Therefore,
-once a merge request gets merged, it will be available online on the same day,
-but they will be shipped (and available on `/help`) within the milestone assigned
+once a merge request gets merged, it will be available online on the same day.
+However, they will be shipped (and available on `/help`) within the milestone assigned
to the MR.
For instance, let's say your merge request has a milestone set to 11.3, which
@@ -614,7 +632,7 @@ They should be placed in a new directory named `/article-title/index.md` under a
- **User guides**: technical content to guide regular users from point A to point B
- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
-- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
+- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach specific objectives
#### Understanding guides, tutorials, and technical overviews
@@ -646,7 +664,6 @@ with the following information:
For example:
-
```yaml
---
author: John Doe
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c43f91278de..8c3ab7643ba 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -15,10 +15,10 @@ For help adhering to the guidelines, see [Linting](index.md#linting).
## Files
- [Directory structure](index.md#location-and-naming-documents): place the docs
-in the correct location.
+ in the correct location.
- [Documentation files](index.md#documentation-files): name the files accordingly.
- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the
-documentation.
+ documentation.
NOTE: **Note:**
**Do not** use capital letters, spaces, or special chars in file names,
@@ -45,10 +45,10 @@ a test that will fail if it spots a new `README.md` file.
- Capitalize "G" and "L" in GitLab.
- Use sentence case for titles, headings, labels, menu items, and buttons.
- Use title case when referring to [features](https://about.gitlab.com/features/) or
-[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
-Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
-(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
-some features are also objects (e.g. "Merge Requests" and "merge requests").
+ [products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
+ Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
+ (e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
+ some features are also objects (e.g. "Merge Requests" and "merge requests").
## Formatting
@@ -130,9 +130,9 @@ To indicate the steps of navigation through the UI:
- Use the exact word as shown in the UI, including any capital letters as-is.
- Use bold text for navigation items and the char `>` as separator
-(e.g., `Navigate to your project's **Settings > CI/CD**` ).
+ (e.g., `Navigate to your project's **Settings > CI/CD**` ).
- If there are any expandable menus, make sure to mention that the user
-needs to expand the tab to find the settings you're referring to.
+ needs to expand the tab to find the settings you're referring to.
## Images
@@ -147,7 +147,7 @@ needs to expand the tab to find the settings you're referring to.
- Compress all images with <https://tinypng.com/> or similar tool.
- Compress gifs with <https://ezgif.com/optimize> or similar tool.
- Images should be used (only when necessary) to _illustrate_ the description
-of a process, not to _replace_ it.
+ of a process, not to _replace_ it.
- Max image size: 100KB (gifs included).
- The GitLab docs do not support videos yet.
@@ -296,7 +296,7 @@ keyword "only":
- For GitLab Core: `**[CORE ONLY]**`.
The tier should be ideally added to headers, so that the full badge will be displayed.
-But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
+However, it can be also mentioned from paragraphs, list items, and table cells. For these cases,
the tier mention will be represented by an orange question mark.
E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
@@ -317,7 +317,7 @@ avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
read like:
-```
+```md
Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
```
@@ -403,19 +403,19 @@ low.
You can use the following fake tokens as examples.
-| **Token type** | **Token value** |
-| --------------------- | --------------------------------- |
-| Private user token | `9koXpg98eAheJpvBs5tK` |
-| Personal access token | `n671WNGecHugsdEDPsyo` |
+| **Token type** | **Token value** |
+|:----------------------|:-------------------------------------------------------------------|
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
-| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
-| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
-| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
-| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
-| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
-| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
-| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
### API
@@ -438,16 +438,16 @@ on this document. Further explanation is given below.
Use the following table headers to describe the methods. Attributes should
always be in code blocks using backticks (``` ` ```).
-```
+```md
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|:----------|:-----|:---------|:------------|
```
Rendered example:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user` | string | yes | The GitLab username |
+| Attribute | Type | Required | Description |
+|:----------|:-------|:---------|:--------------------|
+| `user` | string | yes | The GitLab username |
#### cURL commands
@@ -459,12 +459,12 @@ Rendered example:
- Prefer to use examples using the personal access token and don't pass data of
username and password.
-| Methods | Description |
-| ------- | ----------- |
+| Methods | Description |
+|:-------------------------------------------|:------------------------------------------------------|
| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
-| `-X POST` | Use this method when creating new objects |
-| `-X PUT` | Use this method when updating existing objects |
-| `-X DELETE` | Use this method when removing existing objects |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
#### cURL Examples
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 52bc059a925..75ce8640e87 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -7,16 +7,16 @@ description: Learn the process of shipping documentation for GitLab.
At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process.
- Product Managers (PMs): in the issue for all new and updated features,
-PMs include specific documentation requirements that the developer who is
-writing or updating the docs must meet, along with feature descriptions
-and use cases. They call out any specific areas where collaborating with
-a technical writer is recommended, and usually act as the first reviewer
-of the docs.
+ PMs include specific documentation requirements that the developer who is
+ writing or updating the docs must meet, along with feature descriptions
+ and use cases. They call out any specific areas where collaborating with
+ a technical writer is recommended, and usually act as the first reviewer
+ of the docs.
- Developers: author documentation and merge it on time (up to a week after
-the feature freeze).
+ the feature freeze).
- Technical Writers: review each issue to ensure PM's requirements are complete,
-help developers with any questions throughout the process, and act as the final
-reviewer of all new and updated docs content before it's merged.
+ help developers with any questions throughout the process, and act as the final
+ reviewer of all new and updated docs content before it's merged.
## Requirements
@@ -112,17 +112,17 @@ and the missed-deliverable due date (the 14th of each month) are both respected.
The developer should add to the feature MR the documentation containing:
- The [documentation blurb](structure.md#documentation-blurb): copy the
-feature name, overview/description, and use cases from the feature issue
+ feature name, overview/description, and use cases from the feature issue
- Instructions: write how to use the feature, step by step, with no gaps.
- [Crosslink for discoverability](structure.md#discoverability): link with
-internal docs and external resources (if applicable)
+ internal docs and external resources (if applicable)
- Index: link the new doc or the new heading from the higher-level index
-for [discoverability](#discoverability)
+ for [discoverability](#discoverability)
- [Screenshots](styleguide.md#images): when necessary, add screenshots for:
- Illustrating a step of the process
- Indicating the location of a navigation menu
- Label the MR with `Documentation`, `Deliverable`, `docs-P1`, and assign
-the correct milestone
+ the correct milestone
- Assign the PM for review
- When done, mention the `@gl\-docsteam` in the MR asking for review
- **Due date**: feature freeze date and time
@@ -133,10 +133,10 @@ If the docs aren't being shipped within the feature MR:
- Create a new issue mentioning "docs" or "documentation" in the title (use the Documentation issue description template)
- Label the issue with: `Documentation`, `Deliverable`, `docs-P1`, `<product-label>`
-(product label == CI/CD, Pages, Prometheus, etc)
+ (product label == CI/CD, Pages, Prometheus, etc)
- Add the correct milestone
- Create a new MR for shipping the docs changes and follow the same
-process [described above](#documentation-shipped-in-the-feature-mr)
+ process [described above](#documentation-shipped-in-the-feature-mr)
- Use the MR description template called "Documentation"
- Add the same labels and milestone as you did for the issue
- Assign the PM for review
@@ -162,9 +162,9 @@ The **due date** for **merging** `missed-deliverable` MRs is on the
- **Planning**
- Once an issue contains a Documentation label and the current milestone, a
-technical writer reviews the Product Manager's documentation requirements
+ technical writer reviews the Product Manager's documentation requirements.
- Once the documentation requirements are approved, the technical writer can
-work with the developer to discuss any documentation questions and plans/outlines, as needed.
+ work with the developer to discuss any documentation questions and plans/outlines, as needed.
- **Review** - A technical writer must review the documentation for:
- Clarity
@@ -183,4 +183,3 @@ work with the developer to discuss any documentation questions and plans/outline
- Describe the difference between new features and feature updates
- Creating a new doc vs updating an existing doc
-->
-
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c7b10dfd5ce..e738f2b4b66 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -34,13 +34,14 @@ graphs/dashboards.
## Tooling
-GitLab provides built-in tools to aid the process of improving performance:
+GitLab provides built-in tools to help improve performance and availability:
* [Profiling](profiling.md)
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/index.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
+* [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
GitLab employees can use GitLab.com's performance monitoring systems located at
<https://dashboards.gitlab.net>, this requires you to log in using your
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 4f4ff85fe1d..36d150c8a5b 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -1,75 +1,82 @@
-# Review apps
+# Review Apps
-Review Apps are automatically deployed by each pipeline, both in
+Review Apps are automatically deployed by each pipeline, both in
[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and
[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665).
## How does it work?
-1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
+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] [^1] project
-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
+ [`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] [^2] to the
+ [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 overriden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry]
-1. Once the `review` job succeeds, you should be able to use your review app
+ - 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 overriden 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
- **gitlab-{ce,ee} review app's root password**.
+ **gitlab-{ce,ee} Review App's root password** (note that there's currently
+ [a bug where the default password seems to be overriden][password-bug]).
**Additional notes:**
-- The Kubernetes cluster is connected to the `gitlab-ee` project 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 `post-cleanup` stage can be used to stop a
- review app manually, and is also started by GitLab once a branch is deleted
-- [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs
- the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script
-- If you're unable to log in using the `root` username and password, the
- deployment may have failed. Stop the Review App via the `stop_review`
+- 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.
+- 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.
-[^1]: 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.
-[^2]: Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get the a dedicated environment for your branch that's very close to what it
- would look in production
-
## Frequently Asked Questions
-**Will it be too much to trigger CNG image builds on every test run? This could create thousands of unused docker images.**
+**Isn't it too much to trigger CNG image builds on every test run? This creates
+thousands of unused Docker images.**
- > We have to start somewhere and improve later. If we see this getting out of hand, we will revisit.
+ > We have to start somewhere and improve later. Also, we're using the
+ CNG-mirror project to store these Docker images so that we can just wipe out
+ the registry at some point, and use a new fresh, empty one.
-**How big is the Kubernetes cluster?**
+**How big are the Kubernetes clusters (`review-apps-ce` and `review-apps-ee`)?**
- > The cluster is currently setup with a single pool of preemptible
- nodes, with a minimum of 1 node and a maximum of 30 nodes.
+ > The clusters are currently set up with a single pool of preemptible nodes,
+ with a minimum of 1 node and a maximum of 100 nodes.
**What are the machine running on the cluster?**
> We're currently using `n1-standard-4` (4 vCPUs, 15 GB memory) machines.
-**How do we secure this from abuse? Apps are open to the world so we need to find a way to limit it to only us.**
+**How do we secure this from abuse? Apps are open to the world so we need to
+find a way to limit it to only us.**
- > This won't work for forks. We will add a root password to 1password shared vault.
+ > This isn't enabled for forks.
-[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ee/pipelines/29302122
-[review-job]: https://gitlab.com/gitlab-org/gitlab-ee/-/jobs/94294136
+[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/35850709
+[review-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/29307727
+[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-ee]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps
@@ -77,6 +84,7 @@ Review Apps are automatically deployed by each pipeline, both in
[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-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/development/utilities.md b/doc/development/utilities.md
index 0d074a3ef05..e5466ae8914 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -171,8 +171,8 @@ class Commit
extend Gitlab::Cache::RequestCache
def author
- User.find_by_any_email(author_email.downcase)
+ User.find_by_any_email(author_email)
end
- request_cache(:author) { author_email.downcase }
+ request_cache(:author) { author_email }
end
```
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index acaed53e052..4cb8ca4f3e7 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,15 +1,20 @@
# Database MySQL
-> **Note:**
-> - We do not recommend using MySQL due to various issues. For example, case
- [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
- and [problems](https://bugs.mysql.com/bug.php?id=65830) that
- [suggested](https://bugs.mysql.com/bug.php?id=50909)
- [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
+NOTE: **Note:**
+We do not recommend using MySQL due to various issues.
+For example, there have been bugs with case
+[(in)sensitivity](https://dev.mysql.com/doc/refman/5.7/en/case-sensitivity.html).
+
+Bugs relating to case sensitivity:
+
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=50909>
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=63164>
## Initial database setup
-```
+```sh
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
@@ -84,8 +89,9 @@ GitLab 8.14 has introduced [a feature](https://gitlab.com/gitlab-org/gitlab-ce/m
Follow the below instructions to ensure you use the most up to date requirements for your GitLab MySQL Database.
**We are about to do the following:**
+
- Ensure you can enable `utf8mb4` encoding and `utf8mb4_general_ci` collation for your GitLab DB, tables and data.
-- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`
+- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`.
### Check for utf8mb4 support
@@ -130,7 +136,8 @@ We need to check, enable and maybe convert your existing GitLab DB tables to the
Whatever the results of your checks above, we now need to check if your GitLab database has been created using [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) (i.e. `innodb_file_per_table` was set to **1** at initial setup time).
-> Note: This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
+NOTE: **Note:**
+This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.ibd | wc -l
@@ -138,20 +145,19 @@ Whatever the results of your checks above, we now need to check if your GitLab d
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.frm | wc -l
-
- **Case 1: a result > 0 for both commands**
-Congrats, your GitLab database uses the right InnoDB tablespace format.
+Congratulations, your GitLab database uses the right InnoDB tablespace format.
However, you must still ensure that any **future tables** created by GitLab will still use the right format:
- If `SELECT @@innodb_file_per_table` returned **1** previously, your server is running correctly.
- > It's however a requirement to check *now* that this setting is indeed persisted in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file!
+ > It's however a requirement to check *now* that this setting is indeed persisted in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file!
- If `SELECT @@innodb_file_per_table` returned **0** previously, your server is not running correctly.
- > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file
+ > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file.
Now, if you have a **different result** returned by the 2 commands above, it means you have a **mix of tables format** uses in your GitLab database. This can happen if your MySQL server had different values for `innodb_file_per_table` in its life and you updated GitLab at different moments with those inconsistent values. So keep reading.
@@ -172,7 +178,7 @@ Let's enable what we need on the running server:
# You can now quit the database session
mysql> \q
-> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format-enabling.html) in your `my.cnf` file.
+> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format-enabling.html) in your `my.cnf` file.
Ensure at this stage that your GitLab instance is indeed **stopped**.
@@ -184,7 +190,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
# Type the MySQL root password
mysql > use gitlabhq_production;
- # Safety check: you should still have those values set as follow:
+ # Safety check: you should still have those values set as follows:
mysql> SELECT @@innodb_file_per_table, @@innodb_file_format;
+-------------------------+----------------------+
| @@innodb_file_per_table | @@innodb_file_format |
@@ -203,7 +209,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
#### Check for proper InnoDB File Format, Row Format, Large Prefix and tables conversion
-We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
+We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
# Login to MySQL
mysql -u root -p
@@ -229,7 +235,7 @@ We need to check, enable and probably convert your existing GitLab DB tables to
| utf8 | utf8_general_ci |
+--------------------------+----------------------+
-> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
+> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
#### Tables and data conversion to utf8mb4
@@ -257,7 +263,7 @@ Now that you have a persistent MySQL setup, you can safely upgrade tables after
Ensure your GitLab database configuration file uses a proper connection encoding and collation:
-```sudo -u git -H editor config/database.yml```
+`sudo -u git -H editor config/database.yml`
production:
adapter: mysql2
@@ -266,19 +272,19 @@ Ensure your GitLab database configuration file uses a proper connection encoding
[Restart your GitLab instance](../administration/restart_gitlab.md).
-
## MySQL strings limits
After installation or upgrade, remember to run the `add_limits_mysql` Rake task:
**Omnibus GitLab installations**
-```
+
+```sh
sudo gitlab-rake add_limits_mysql
```
**Installations from source**
-```
+```sh
bundle exec rake add_limits_mysql RAILS_ENV=production
```
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 37c826ce9e0..316411d1047 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-4-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-5-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-4-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-5-stable gitlab
-**Note:** You can change `11-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 6d1bc4aedc4..3f5b36f7254 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -31,7 +31,7 @@ to deploy.
TIP: **Tip:**
For production deployments, we strongly recommend using the
-[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/README.md)
+[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/index.md)
utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services.
### Requirements
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 5c8a830ac8f..4c88b6f97fc 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -505,7 +505,7 @@ PaaS and managing your applications with the ease of containers.
[RedHat]: https://www.redhat.com/en "RedHat website"
[openshift]: https://www.openshift.org "OpenShift Origin website"
[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM"
-[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas"
+[vm-new]: https://app.vagrantup.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Vagrant Cloud"
[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab"
[openshift.com]: https://openshift.com "OpenShift Online"
[kubernetes]: http://kubernetes.io/ "Kubernetes website"
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 97e9b36d1a6..bb316df5b9a 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -6,6 +6,7 @@
- The groups will be created as needed, including subgroups
- The owner of the group will be the first admin
- Existing projects will be skipped
+- Projects in hashed storage may be skipped (see [Importing bare repositories from hashed storage](#importing-bare-repositories-from-hashed-storage))
- The existing Git repos will be moved from disk (removed from the original path)
## How to use
@@ -26,7 +27,6 @@ sudo -u git mkdir /var/opt/gitlab/git-data/repository-import-<date>/new_group
If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
-
```
sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
@@ -70,3 +70,73 @@ Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
* Skipping repo /var/opt/gitlab/git-data/repository-import-1/@shared/a/b/abcd.git
[...]
```
+
+## Importing bare repositories from hashed storage
+
+### Background
+
+Projects in legacy storage have a directory structure that mirrors their full
+project path in GitLab, including their namespace structure. This information is
+leveraged by the bare repository importer to import projects into their proper
+locations. Each project and its parent namespaces are meaningfully named.
+
+However, the directory structure of projects in hashed storage do not contain
+this information. This is beneficial for a variety of reasons, especially
+improved performance and data integrity. See
+[Repository Storage Types](../administration/repository_storage_types.md) for
+more details.
+
+### Which repositories are importable?
+
+#### GitLab 10.3 or earlier
+
+Importing bare repositories from hashed storage is unsupported.
+
+#### GitLab 10.4 and later
+
+To support importing bare repositories from hashed storage, GitLab 10.4 and
+later stores the full project path with each repository, in a special section of
+the git repository's config file. This section is formatted as follows:
+
+```
+[gitlab]
+ fullpath = gitlab-org/gitlab-ce
+```
+
+However, existing repositories were not migrated to include this path.
+
+Bare repositories are importable if the following events occurred to the
+repository in GitLab 10.4 and later:
+
+- Created
+- Migrated to hashed storage
+- Renamed
+- Transferred to another namespace
+- Ancestor renamed
+- Ancestor transferred to another namespace
+
+Bare repositories are **not** importable by GitLab 10.4 and later when all the following are true about the repository:
+
+- It was created in GitLab 10.3 or earlier.
+- It was not renamed, transferred, or migrated to hashed storage in GitLab 10.4 and later.
+- Its ancestor namespaces were not renamed or transferred in GitLab 10.4 and later.
+
+There is an [open issue to add a migration to make all bare repositories
+importable](https://gitlab.com/gitlab-org/gitlab-ce/issues/41776).
+
+Until then, you may wish to manually migrate repositories yourself. You can use
+[Rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session)
+to do so. In a Rails console session, run the following to migrate a project:
+
+```
+project = Project.find_by_full_path('gitlab-org/gitlab-ce')
+project.write_repository_config
+```
+
+In a Rails console session, run the following to migrate all of a namespace's
+projects (this may take a while if there are 1000s of projects in a namespace):
+
+```
+namespace = Namespace.find_by_full_path('gitlab-org')
+namespace.send(:write_projects_repository_config)
+```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 73301394e9f..394f3ea60b7 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -36,7 +36,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/README.md#oauth-2-tokens)
-- [Private Tokens](../../api/README.md#private-tokens)
+- [Personal access tokens](../../api/README.md#personal-access-tokens)
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
diff --git a/doc/university/README.md b/doc/university/README.md
index f19b1ffd3d9..3e7d02770e4 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -104,7 +104,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Due Dates and Milestones for GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
-1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/product/issueboard/)
1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
@@ -125,7 +125,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
-2. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
+1. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. See **[Integrations](#39-integrations)** for integrations with other CI services.
#### 2.4. Workflow
@@ -140,7 +140,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
-1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03)](https://youtu.be/Nbzp1t45ERo)
1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
@@ -189,7 +189,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.8 Cycle Analytics
1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
-1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/product/cycle-analytics/)
#### 3.9. Integrations
@@ -213,7 +213,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
### 5. Resources for GitLab Team Members
-*Some content can only be accessed by GitLab team members*
+NOTE: **Note:**
+Some content can only be accessed by GitLab team members
1. [Support Path](support/README.md)
1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index d01634df744..4871372d105 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -4,9 +4,9 @@ comments: false
# Additional Resources
-1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
-2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
-3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
-4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
-5. Code School tutorial [http://try.github.io/](http://try.github.io/)
-6. Contact Us at `subscribers@gitlab.com`
+1. GitLab Documentation: <http://docs.gitlab.com>.
+1. GUI Clients: <http://git-scm.com/downloads/guis>.
+1. Pro Git book: <http://git-scm.com/book>.
+1. Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
+1. Code School tutorial: <http://try.github.io/>.
+1. Contact us at `subscribers@gitlab.com`.
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index 6333ceedbd7..9526bcbfb82 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -22,7 +22,7 @@ comments: false
**Additional resources**
-[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+<http://git-scm.com/book/en/Git-Basics-Tagging>
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index f3a4d766522..ca3f777f403 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -385,7 +385,7 @@ Thank you for your hard work!
- GitLab Documentation: <http://docs.gitlab.com/>.
- GUI Clients: <http://git-scm.com/downloads/guis>.
-- Pro git book: <http://git-scm.com/book>.
+- Pro Git book: <http://git-scm.com/book>.
- Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
- Code School tutorial: <http://try.github.io/>.
- Contact us at `subscribers@gitlab.com`.
diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md
index e64ab2acae2..44105348d14 100644
--- a/doc/update/11.4-to-11.5.md
+++ b/doc/update/11.4-to-11.5.md
@@ -91,11 +91,11 @@ Download and install Go:
# Remove former Go installation folder
sudo rm -rf /usr/local/go
-curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
-echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
- sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+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.3.linux-amd64.tar.gz
+rm go1.10.5.linux-amd64.tar.gz
```
### 6. Get latest code
@@ -153,20 +153,6 @@ sudo -u git -H make
### 9. Update Gitaly
-#### New Gitaly configuration options required
-
-In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
-
-```shell
-echo '
-[gitaly-ruby]
-dir = "/home/git/gitaly/ruby"
-
-[gitlab-shell]
-dir = "/home/git/gitlab-shell"
-' | sudo -u git tee -a /home/git/gitaly/config.toml
-```
-
#### Check Gitaly configuration
Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
@@ -272,10 +258,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes:
cd /home/git/gitlab
# For HTTPS configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
+git diff origin/11-4-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
+git diff origin/11-4-stable:lib/support/nginx/gitlab origin/11-5-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
@@ -309,7 +295,7 @@ There might be new configuration options available for [`gitlab.default.example`
```sh
cd /home/git/gitlab
-git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
+git diff origin/11-4-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
```
Ensure you're still up-to-date with the latest init script changes:
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 7c9e5bf882e..50c318a4969 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -3,3 +3,20 @@
## Custom logo
The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
+
+## Custom hostname for private commit emails
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
+
+This configuration option sets the email hostname for [private commit emails](../../profile/index.md#private-commit-email),
+and it's, by default, set to `users.noreply.YOUR_CONFIGURED_HOSTNAME`.
+
+In order to change this option:
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+1. Under the **Email** section, change the **Custom hostname (for private commit emails)** field.
+1. Hit **Save** for the changes to take effect.
+
+NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get
+recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as
+`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ab62762f343..da7c30b6b39 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -31,6 +31,7 @@ From there, you can:
- Update your personal information
- Set a [custom status](#current-status) for your profile
+- Manage your [commit email](#commit-email) for your profile
- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
@@ -132,6 +133,45 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`.
You can also set your current status [using the API](../../api/users.md#user-status).
+## Commit email
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21598) in GitLab 11.4.
+
+A commit email, is the email that will be displayed in every Git-related action done through the
+GitLab interface.
+
+You are able to select from the list of your own verified emails which email you want to use as the commit email.
+
+To change it:
+
+1. Open the user menu in the top-right corner of the navigation bar.
+1. Hit **Commit email** selection box.
+1. Select any of the verified emails.
+1. Hit **Update profile settings**.
+
+### Private commit email
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
+
+GitLab provides the user with an automatically generated private commit email option,
+which allows the user to not make their email information public.
+
+To enable this option:
+
+1. Open the user menu in the top-right corner of the navigation bar.
+1. Hit **Commit email** selection box.
+1. Select **Use a private email** option.
+1. Hit **Update profile settings**.
+
+Once this option is enabled, every Git-related action will be performed using the private commit email.
+
+In order to stay fully annonymous, you can also copy this private commit email
+and configure it on your local machine using the following command:
+
+```
+git config --global user.email "YOUR_PRIVATE_COMMIT_EMAIL"
+```
+
## Troubleshooting
### Why do I keep getting signed out?
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index 10f0cdb333e..ef19b05fb9e 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -13,11 +13,13 @@ date: 2018-06-05
In this tutorial, we will show how easy it is to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications.
For an end-to-end walkthrough we will:
+
1. Start with a new project based on the sample Ruby on Rails template
1. Integrate an EKS cluster
1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application
You will need:
+
1. An account on GitLab, like [GitLab.com](https://gitlab.com)
1. An Amazon EKS cluster
1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl)
@@ -49,11 +51,12 @@ A few details from the EKS cluster will be required to connect it to GitLab.
1. The API server endpoint is also required, so GitLab can connect to the cluster. This is displayed on the AWS EKS console, when viewing the EKS cluster details.
You now have all the information needed to connect the EKS cluster:
+
* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
* API URL: Paste in the API server endpoint retrieved above.
* CA Certificate: Paste the certificate data from the earlier step, as-is.
-* Paste the token value. Note on some versions of Kubernetes a trailing `%` is output, do not include it.
+* Paste the token value.
* Project namespace: This can be left blank to accept the default namespace, based on the project name.
![Add Cluster](img/add_cluster.png)
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 762d254d6cc..233ed205790 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -132,59 +132,62 @@ functionalities needed to successfully build and deploy a containerized
application. Bare in mind that the same credentials are used for all the
applications running on the cluster.
-When GitLab creates the cluster, it enables and uses the legacy
-[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/).
-The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)
-authorization is [experimental](#role-based-access-control-rbac).
-
-### Role-based access control (RBAC) **[CORE ONLY]**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21401) in GitLab 11.4.
-
-CAUTION: **Warning:**
-The RBAC authorization is experimental.
-
-Once RBAC is enabled for a cluster, GitLab will create the necessary service accounts
-and privileges in order to install and run [GitLab managed applications](#installing-applications).
-
-If you are creating a [new GKE cluster via
-GitLab](#adding-and-creating-a-new-gke-cluster-via-gitlab), you will be
-asked if you would like to create an RBAC-enabled cluster. Enabling this
-setting will create a `gitlab` service account which will be used by
-GitLab to manage the newly created cluster. To enable this, this service
-account will have the `cluster-admin` privilege.
-
-If you are [adding an existing Kubernetes
-cluster](#adding-an-existing-kubernetes-cluster), you will be asked if
-the cluster you are adding is a RBAC-enabled cluster. Ensure the
-token of the account has administrator privileges for the cluster.
-
-In both cases above, when you install Helm Tiller into your cluster, an
-RBAC-enabled cluster will create a `tiller` service account, with `cluster-admin`
-privileges in the `gitlab-managed-apps` namespace. This service account will be
-added to the installed Helm Tiller and will be used by Helm to install and run
-[GitLab managed applications](#installing-applications).
-
-The table below summarizes which resources will be created in a
-RBAC-enabled cluster :
-
-| Name | Kind | Details | Created when |
-| --- | --- | --- | --- |
-| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
-| `gitlab-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Creating a new GKE Cluster |
-| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
-| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
-| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
-
-
-Helm Tiller will also create additional service accounts and other RBAC
-resources for each installed application. Consult the documentation for the
-Helm charts for each application for details.
-
-NOTE: **Note:**
-Auto DevOps will not successfully complete in a cluster that only has RBAC
-authorization enabled. RBAC support for Auto DevOps is planned in a
-[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/44597).
+## Access controls
+
+When creating a cluster in GitLab, you will be asked if you would like to create an
+[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster, or
+a [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) one.
+
+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.
+
+- A project service account with `edit` privileges will be created in
+ the project namespace (also created by GitLab), which will be used in
+ [deployment jobs](#deployment-variables).
+
+ NOTE: **Note:**
+ Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
+
+- When you install Helm Tiller into your cluster, the `tiller` service account
+ will be created with `cluster-admin` privileges in the `gitlab-managed-apps`
+ namespace. This service account will be added to the installed Helm Tiller and will
+ be used by Helm to install and run [GitLab managed applications](#installing-applications).
+ Helm Tiller will also create additional service accounts and other resources for each
+ installed application. Consult the documentation of the Helm charts for each application
+ for details.
+
+If you are [adding an existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster),
+ensure the token of the account has administrator privileges for the cluster.
+
+The following sections summarize which resources will be created on ABAC/RBAC clusters.
+
+### Attribute-based access control (ABAC)
+
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
+
+### Role-based access control (RBAC)
+
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
+| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating/Adding a new GKE Cluster |
### Security of GitLab Runners
@@ -211,7 +214,7 @@ 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:**
-The applications will be installed in a dedicated namespace called
+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
@@ -224,6 +227,7 @@ twice, which can lead to confusion during deployments.
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
+| [Knative](https://cloud.google.com/knative) | 0.1.2 | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as <program_name>.<kubernetes_namespace>.<domain_name>. **Note**: This will require your kubernetes cluster to have RBAC enabled. | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
## Getting the external IP address
@@ -232,6 +236,10 @@ 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.
+
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
@@ -262,6 +270,12 @@ run the following command:
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
+NOTE: **Note:**
+For Istio/Knative, the command will be different:
+```bash
+kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
+```
+
Otherwise, you can list the IP addresses of all load balancers:
```bash
@@ -376,12 +390,16 @@ GitLab CI/CD build environment.
| Variable | Description |
| -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. |
-| `KUBE_TOKEN` | The Kubernetes token. |
+| `KUBE_TOKEN` | The Kubernetes token of the [project service account](#access-controls). |
| `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` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. |
-| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
+| `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. |
+NOTE: **NOTE:**
+Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
+service account of the cluster integration.
+
## 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/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 0a7f7d37384..6de2ab07fc4 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -166,6 +166,23 @@ administrator to do so.
![Create new merge requests by email](img/create_from_email.png)
+### Adding patches when creating a merge request via e-mail
+
+> **Note**: This feature was [implemented in GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22723)
+
+You can add commits to the merge request being created by adding
+patches as attachments to the email, all attachments with a filename
+ending in `.patch` will be considered patches. The patches will be processed
+ordered by name.
+
+The combined size of the patches can be 2MB.
+
+If the source branch from the subject does not exist, it will be
+created from the repository's HEAD or the specified target branch to
+apply the patches. The target branch can be specified using the
+[`/target_branch` quick action](../quick_actions.md). If the source
+branch already exists, the patches will be applied on top of it.
+
## Find the merge request that introduced a change
> **Note**: this feature was [implemented in GitLab 10.5](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c49c52213bf..8e259961828 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -143,6 +143,7 @@ module API
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
+ mount ::API::Submodules
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e59abd3e3d0..1b228069005 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -204,6 +204,40 @@ module API
end
end
+ desc 'Revert a commit in a branch' do
+ detail 'This feature was introduced in GitLab 11.6'
+ success Entities::Commit
+ end
+ params do
+ requires :sha, type: String, desc: 'Commit SHA to revert'
+ requires :branch, type: String, desc: 'Target branch name', allow_blank: false
+ end
+ post ':id/repository/commits/:sha/revert', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ authorize_push_to_branch!(params[:branch])
+
+ commit = user_project.commit(params[:sha])
+ not_found!('Commit') unless commit
+
+ find_branch!(params[:branch])
+
+ commit_params = {
+ commit: commit,
+ start_branch: params[:branch],
+ branch_name: params[:branch]
+ }
+
+ result = ::Commits::RevertService
+ .new(user_project, current_user, commit_params)
+ .execute
+
+ if result[:status] == :success
+ present user_project.repository.commit(result[:result]),
+ with: Entities::Commit
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
desc 'Get all references a commit is pushed to' do
detail 'This feature was introduced in GitLab 10.6'
success Entities::BasicRef
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9f7be27b047..61d57c643f0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -607,6 +607,22 @@ module API
end
class MergeRequestBasic < ProjectEntity
+ expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.merged_by
+ end
+
+ expose :merged_at do |merge_request, _options|
+ merge_request.metrics&.merged_at
+ end
+
+ expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.latest_closed_by
+ end
+
+ expose :closed_at do |merge_request, _options|
+ merge_request.metrics&.latest_closed_at
+ end
+
expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
MarkupHelper.markdown_field(entity, :title)
end
@@ -676,22 +692,6 @@ module API
merge_request.merge_request_diff.real_size
end
- expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.merged_by
- end
-
- expose :merged_at do |merge_request, _options|
- merge_request.metrics&.merged_at
- end
-
- expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
- merge_request.metrics&.latest_closed_by
- end
-
- expose :closed_at do |merge_request, _options|
- merge_request.metrics&.latest_closed_at
- end
-
expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
merge_request.metrics&.latest_build_started_at
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 7909f9c7a00..491b5085bb8 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -28,7 +28,7 @@ module API
args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
- .preload(:assignees, :labels, :notes, :timelogs, :project, :author)
+ .preload(:assignees, :labels, :notes, :timelogs, :project, :author, :closed_by)
issues.reorder(args[:order_by] => args[:sort])
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index a617efaaa4c..16f07f16387 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -45,7 +45,7 @@ module API
return merge_requests if args[:view] == 'simple'
merge_requests
- .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs)
+ .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs, metrics: [:latest_closed_by, :merged_by])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index f1786c15f4f..1ae144ca9c1 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -14,7 +14,7 @@ module API
end
def public_snippets
- SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
+ SnippetsFinder.new(current_user, scope: :are_public).execute
end
end
diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb
new file mode 100644
index 00000000000..72d7d994102
--- /dev/null
+++ b/lib/api/submodules.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module API
+ class Submodules < Grape::API
+ before { authenticate! }
+
+ helpers do
+ def commit_params(attrs)
+ {
+ submodule: attrs[:submodule],
+ commit_sha: attrs[:commit_sha],
+ branch_name: attrs[:branch],
+ commit_message: attrs[:commit_message]
+ }
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ desc 'Update existing submodule reference in repository' do
+ success Entities::Commit
+ end
+ params do
+ requires :submodule, type: String, desc: 'Url encoded full path to submodule.'
+ requires :commit_sha, type: String, desc: 'Commit sha to update the submodule to.'
+ requires :branch, type: String, desc: 'Name of the branch to commit into.'
+ optional :commit_message, type: String, desc: 'Commit message. If no message is provided a default one will be set.'
+ end
+ put ":id/repository/submodules/:submodule", requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ authorize! :push_code, user_project
+
+ submodule_params = declared_params(include_missing: false)
+
+ result = ::Submodules::UpdateService.new(user_project, current_user, commit_params(submodule_params)).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commit(result[:result])
+ present commit_detail, with: Entities::CommitDetail
+ else
+ render_api_error!(result[:message], result[:http_status] || 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb
index 15e59f93141..83e8808db07 100644
--- a/lib/bitbucket_server/client.rb
+++ b/lib/bitbucket_server/client.rb
@@ -35,9 +35,9 @@ module BitbucketServer
BitbucketServer::Representation::Repo.new(parsed_response)
end
- def repos
+ def repos(page_offset: 0, limit: nil)
path = "/repos"
- get_collection(path, :repo)
+ get_collection(path, :repo, page_offset: page_offset, limit: limit)
end
def create_branch(project_key, repo, branch_name, sha)
@@ -61,8 +61,8 @@ module BitbucketServer
private
- def get_collection(path, type)
- paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type)
+ def get_collection(path, type, page_offset: 0, limit: nil)
+ paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit)
BitbucketServer::Collection.new(paginator)
rescue *SERVER_ERRORS => e
raise ServerError, e
diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb
index b50c5dde352..7e4b2277bbe 100644
--- a/lib/bitbucket_server/collection.rb
+++ b/lib/bitbucket_server/collection.rb
@@ -2,7 +2,13 @@
module BitbucketServer
class Collection < Enumerator
+ attr_reader :paginator
+
+ delegate :page_offset, :has_next_page?, to: :paginator
+
def initialize(paginator)
+ @paginator = paginator
+
super() do |yielder|
loop do
paginator.items.each { |item| yielder << item }
@@ -12,6 +18,24 @@ module BitbucketServer
lazy
end
+ def current_page
+ return 1 if page_offset <= 1
+
+ [1, page_offset].max
+ end
+
+ def prev_page
+ return nil unless current_page > 1
+
+ current_page - 1
+ end
+
+ def next_page
+ return nil unless has_next_page?
+
+ current_page + 1
+ end
+
def method_missing(method, *args)
return super unless self.respond_to?(method)
diff --git a/lib/bitbucket_server/paginator.rb b/lib/bitbucket_server/paginator.rb
index c351fb2f11f..aa5f84f44b3 100644
--- a/lib/bitbucket_server/paginator.rb
+++ b/lib/bitbucket_server/paginator.rb
@@ -4,34 +4,49 @@ module BitbucketServer
class Paginator
PAGE_LENGTH = 25
- def initialize(connection, url, type)
+ attr_reader :page_offset
+
+ def initialize(connection, url, type, page_offset: 0, limit: nil)
@connection = connection
@type = type
@url = url
@page = nil
+ @page_offset = page_offset
+ @limit = limit || PAGE_LENGTH
+ @total = 0
end
def items
raise StopIteration unless has_next_page?
+ raise StopIteration if over_limit?
@page = fetch_next_page
+ @total += @page.items.count
@page.items
end
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
private
- attr_reader :connection, :page, :url, :type
+ attr_reader :connection, :page, :url, :type, :limit
- def has_next_page?
- page.nil? || page.next?
+ def over_limit?
+ @limit.positive? && @total >= @limit
end
def next_offset
- page.nil? ? 0 : page.next
+ page.nil? ? starting_offset : page.next
+ end
+
+ def starting_offset
+ [0, page_offset - 1].max * limit
end
def fetch_next_page
- parsed_response = connection.get(@url, start: next_offset, limit: PAGE_LENGTH)
+ parsed_response = connection.get(@url, start: next_offset, limit: @limit)
Page.new(parsed_response, type)
end
end
diff --git a/lib/gitlab/background_migration/remove_restricted_todos.rb b/lib/gitlab/background_migration/remove_restricted_todos.rb
index 9941c2fe6d9..47579d46c1b 100644
--- a/lib/gitlab/background_migration/remove_restricted_todos.rb
+++ b/lib/gitlab/background_migration/remove_restricted_todos.rb
@@ -67,7 +67,7 @@ module Gitlab
.where('access_level >= ?', 20)
confidential_issues = Issue.select(:id, :author_id).where(confidential: true, project_id: project_id)
- confidential_issues.each_batch(of: 100) do |batch|
+ confidential_issues.each_batch(of: 100, order_hint: :confidential) do |batch|
batch.each do |issue|
assigned_users = IssueAssignee.select(:user_id).where(issue_id: issue.id)
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index e4610faa327..c8cb3248fa7 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services
allow_failure type stage when start_in artifacts cache
dependencies before_script after_script variables
- environment coverage retry extends].freeze
+ environment coverage retry parallel extends].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -26,14 +26,13 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
- validates :retry, numericality: { only_integer: true,
- greater_than_or_equal_to: 0,
- less_than_or_equal_to: 2 }
+ validates :parallel, numericality: { only_integer: true,
+ greater_than_or_equal_to: 2,
+ less_than_or_equal_to: 50 }
validates :when,
inclusion: { in: %w[on_success on_failure always manual delayed],
message: 'should be on_success, on_failure, ' \
'always, manual or delayed' }
-
validates :dependencies, array_of_strings: true
validates :extends, type: String
end
@@ -79,17 +78,21 @@ module Gitlab
description: 'Artifacts configuration for this job.'
entry :environment, Entry::Environment,
- description: 'Environment configuration for this job.'
+ description: 'Environment configuration for this job.'
entry :coverage, Entry::Coverage,
- description: 'Coverage configuration for this job.'
+ description: 'Coverage configuration for this job.'
+
+ entry :retry, Entry::Retry,
+ description: 'Retry configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment, :coverage, :retry
+ :artifacts, :commands, :environment, :coverage, :retry,
+ :parallel
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :retry, :extends, :start_in
+ :retry, :parallel, :extends, :start_in
def compose!(deps = nil)
super do
@@ -157,7 +160,8 @@ module Gitlab
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
- retry: retry_defined? ? retry_value.to_i : nil,
+ retry: retry_defined? ? retry_value : nil,
+ parallel: parallel_defined? ? parallel_value.to_i : nil,
artifacts: artifacts_value,
after_script: after_script_value,
ignore: ignored? }
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
new file mode 100644
index 00000000000..e39cc5de229
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a retry config for a job.
+ #
+ class Retry < Simplifiable
+ strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
+ strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
+
+ class SimpleRetry < Entry::Node
+ include Entry::Validatable
+
+ validations do
+ validates :config, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+ end
+
+ def value
+ {
+ max: config
+ }
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class FullRetry < Entry::Node
+ include Entry::Validatable
+ include Entry::Attributable
+
+ ALLOWED_KEYS = %i[max when].freeze
+ attributes :max, :when
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :max, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+
+ validates :when, array_of_strings_or_string: true
+ validates :when,
+ allowed_array_values: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(Array) }
+ validates :when,
+ inclusion: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(String) }
+ end
+ end
+
+ def self.possible_retry_when_values
+ @possible_retry_when_values ||= ::Ci::Build.failure_reasons.keys.map(&:to_s) + ['always']
+ end
+
+ def value
+ super.tap do |config|
+ # make sure that `when` is an array, because we allow it to
+ # be passed as a String in config for simplicity
+ config[:when] = Array.wrap(config[:when]) if config[:when]
+ end
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class UnknownStrategy < Entry::Node
+ def errors
+ ["#{location} has to be either an integer or a hash"]
+ end
+
+ def location
+ 'retry config'
+ end
+ end
+
+ def self.default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 805d26ca8d8..a1d552fb2e5 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -7,11 +7,11 @@ module Gitlab
module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- unknown_keys = record.config.try(:keys).to_a - options[:in]
+ unknown_keys = value.try(:keys).to_a - options[:in]
if unknown_keys.any?
- record.errors.add(:config, 'contains unknown keys: ' +
- unknown_keys.join(', '))
+ record.errors.add(attribute, "contains unknown keys: " +
+ unknown_keys.join(', '))
end
end
end
@@ -24,6 +24,16 @@ module Gitlab
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
@@ -68,6 +78,14 @@ module Gitlab
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
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
new file mode 100644
index 00000000000..b7743bd2090
--- /dev/null
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ class Normalizer
+ def initialize(jobs_config)
+ @jobs_config = jobs_config
+ end
+
+ def normalize_jobs
+ extract_parallelized_jobs!
+ return @jobs_config if @parallelized_jobs.empty?
+
+ parallelized_config = parallelize_jobs
+ parallelize_dependencies(parallelized_config)
+ end
+
+ private
+
+ def extract_parallelized_jobs!
+ @parallelized_jobs = {}
+
+ @jobs_config.each do |job_name, config|
+ if config[:parallel]
+ @parallelized_jobs[job_name] = self.class.parallelize_job_names(job_name, config[:parallel])
+ end
+ end
+
+ @parallelized_jobs
+ end
+
+ def parallelize_jobs
+ @jobs_config.each_with_object({}) do |(job_name, config), hash|
+ if @parallelized_jobs.key?(job_name)
+ @parallelized_jobs[job_name].each { |name, index| hash[name.to_sym] = config.merge(name: name, instance: index) }
+ else
+ hash[job_name] = config
+ end
+
+ hash
+ end
+ end
+
+ def parallelize_dependencies(parallelized_config)
+ parallelized_job_names = @parallelized_jobs.keys.map(&:to_s)
+ parallelized_config.each_with_object({}) do |(job_name, config), hash|
+ if config[:dependencies] && (intersection = config[:dependencies] & parallelized_job_names).any?
+ deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten
+ hash[job_name] = config.merge(dependencies: deps)
+ else
+ hash[job_name] = config
+ end
+
+ hash
+ end
+ end
+
+ def self.parallelize_job_names(name, total)
+ Array.new(total) { |index| ["#{name} #{index + 1}/#{total}", index + 1] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb
index f443dbee120..b3452eae189 100644
--- a/lib/gitlab/ci/status/build/scheduled.rb
+++ b/lib/gitlab/ci/status/build/scheduled.rb
@@ -9,7 +9,7 @@ module Gitlab
{
image: 'illustrations/illustrations_scheduled-job_countdown.svg',
size: 'svg-394',
- title: _("This is a delayed to run in ") + " #{execute_in}",
+ title: _("This is a delayed job to run in %{remainingTime}"),
content: _("This job will automatically run after it's timer finishes. " \
"Often they are used for incremental roll-out deploys " \
"to production environments. When unscheduled it converts " \
@@ -18,21 +18,12 @@ module Gitlab
end
def status_tooltip
- "delayed manual action (#{execute_in})"
+ "delayed manual action (%{remainingTime})"
end
def self.matches?(build, user)
build.scheduled? && build.scheduled_at
end
-
- private
-
- include TimeHelper
-
- def execute_in
- remaining_seconds = [0, subject.scheduled_at - Time.now].max
- duration_in_numbers(remaining_seconds)
- end
end
end
end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 39a1b52e531..e6ec400e476 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -52,6 +52,8 @@ module Gitlab
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry],
+ parallel: job[:parallel],
+ instance: job[:instance],
start_in: job[:start_in]
}.compact }
end
@@ -104,7 +106,7 @@ module Gitlab
##
# Jobs
#
- @jobs = @ci_config.jobs
+ @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
@jobs.each do |name, job|
# logical validation for job
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
index e68ae60ff98..5772727e855 100644
--- a/lib/gitlab/email/handler/create_merge_request_handler.rb
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -44,10 +44,26 @@ module Gitlab
@project ||= Project.find_by_full_path(project_path)
end
+ def metrics_params
+ super.merge(includes_patches: patch_attachments.any?)
+ end
+
private
+ def build_merge_request
+ MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ end
+
def create_merge_request
- merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ merge_request = build_merge_request
+
+ if patch_attachments.any?
+ apply_patches_to_source_branch(start_branch: merge_request.target_branch)
+ remove_patch_attachments
+ # Rebuild the merge request as the source branch might just have
+ # been created, so we should re-validate.
+ merge_request = build_merge_request
+ end
if merge_request.errors.any?
merge_request
@@ -59,12 +75,42 @@ module Gitlab
def merge_request_params
params = {
source_project_id: project.id,
- source_branch: mail.subject,
+ source_branch: source_branch,
target_project_id: project.id
}
params[:description] = message if message.present?
params
end
+
+ def apply_patches_to_source_branch(start_branch:)
+ patches = patch_attachments.map { |patch| patch.body.decoded }
+
+ result = Commits::CommitPatchService
+ .new(project, author, branch_name: source_branch, patches: patches, start_branch: start_branch)
+ .execute
+
+ if result[:status] != :success
+ message = "Could not apply patches to #{source_branch}:\n#{result[:message]}"
+ raise InvalidAttachment, message
+ end
+ end
+
+ def remove_patch_attachments
+ patch_attachments.each { |patch| mail.parts.delete(patch) }
+ # reset the message, so it needs to be reporocessed when the attachments
+ # have been modified
+ @message = nil
+ end
+
+ def patch_attachments
+ @patches ||= mail.attachments
+ .select { |attachment| attachment.filename.ends_with?('.patch') }
+ .sort_by(&:filename)
+ end
+
+ def source_branch
+ @source_branch ||= mail.subject
+ end
end
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index ec4a936e073..d28f6b301fa 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -20,6 +20,7 @@ module Gitlab
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
+ InvalidAttachment = Class.new(ProcessingError)
class Receiver
def initialize(raw)
diff --git a/lib/gitlab/git/patches/collection.rb b/lib/gitlab/git/patches/collection.rb
new file mode 100644
index 00000000000..ad6b5d32abc
--- /dev/null
+++ b/lib/gitlab/git/patches/collection.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Collection
+ MAX_PATCH_SIZE = 2.megabytes
+
+ def initialize(one_or_more_patches)
+ @patches = Array(one_or_more_patches).map do |patch_content|
+ Gitlab::Git::Patches::Patch.new(patch_content)
+ end
+ end
+
+ def content
+ @patches.map(&:content).join("\n")
+ end
+
+ def valid_size?
+ size < MAX_PATCH_SIZE
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ # `@patches` is not an `ActiveRecord` relation, but an `Enumerable`
+ # We're using sum from `ActiveSupport`
+ def size
+ @size ||= @patches.sum(&:size)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/commit_patches.rb b/lib/gitlab/git/patches/commit_patches.rb
new file mode 100644
index 00000000000..c62994432d3
--- /dev/null
+++ b/lib/gitlab/git/patches/commit_patches.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class CommitPatches
+ include Gitlab::Git::WrapsGitalyErrors
+
+ def initialize(user, repository, branch, patch_collection)
+ @user, @repository, @branch, @patches = user, repository, branch, patch_collection
+ end
+
+ def commit
+ repository.with_cache_hooks do
+ wrapped_gitaly_errors do
+ operation_service.user_commit_patches(user, branch, patches.content)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :user, :repository, :branch, :patches
+
+ def operation_service
+ repository.raw.gitaly_operation_client
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/patch.rb b/lib/gitlab/git/patches/patch.rb
new file mode 100644
index 00000000000..fe6ae1b5b00
--- /dev/null
+++ b/lib/gitlab/git/patches/patch.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Patch
+ attr_reader :content
+
+ def initialize(content)
+ @content = content
+ end
+
+ def size
+ content.bytesize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 20cd257bb98..1642c4c5687 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -571,6 +571,20 @@ module Gitlab
end
end
+ def update_submodule(user:, submodule:, commit_sha:, message:, branch:)
+ args = {
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ }
+
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_update_submodule(args)
+ end
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
wrapped_gitaly_errors do
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 0f148614b20..4c78b790ce5 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -230,6 +230,32 @@ module Gitlab
response.squash_sha
end
+ def user_update_submodule(user:, submodule:, commit_sha:, branch:, message:)
+ request = Gitaly::UserUpdateSubmoduleRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_sha: commit_sha,
+ branch: encode_binary(branch),
+ submodule: encode_binary(submodule),
+ commit_message: encode_binary(message)
+ )
+
+ response = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_update_submodule,
+ request
+ )
+
+ if response.pre_receive_error.present?
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
+ elsif response.commit_error.present?
+ raise Gitlab::Git::CommitError, response.commit_error
+ else
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+ end
+
def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
@@ -273,6 +299,29 @@ module Gitlab
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
+ def user_commit_patches(user, branch_name, patches)
+ header = Gitaly::UserApplyPatchRequest::Header.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ target_branch: encode_binary(branch_name)
+ )
+ reader = binary_stringio(patches)
+
+ chunks = Enumerator.new do |chunk|
+ chunk.yield Gitaly::UserApplyPatchRequest.new(header: header)
+
+ until reader.eof?
+ patch_chunk = reader.read(MAX_MSG_SIZE)
+
+ chunk.yield(Gitaly::UserApplyPatchRequest.new(patches: patch_chunk))
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_apply_patch, chunks)
+
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+
private
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 008cba9d33c..f2ce24fefa1 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -10,7 +10,7 @@ module Gitlab
def generate_script
<<~HEREDOC
- set -eo pipefail
+ set -xeo pipefail
HEREDOC
end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
index c7046a9ea75..6691080deca 100644
--- a/lib/gitlab/kubernetes/helm/init_command.rb
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -45,7 +45,7 @@ module Gitlab
def init_helm_command
command = %w[helm init] + init_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
end
def init_command_flags
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index 1be7924d6ac..ff1c1657b98 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -4,22 +4,27 @@ module Gitlab
class InstallCommand
include BaseCommand
- attr_reader :name, :files, :chart, :version, :repository
+ attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall
- def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil)
+ def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name
@chart = chart
@version = version
@rbac = rbac
@files = files
@repository = repository
+ @preinstall = preinstall
+ @postinstall = postinstall
end
def generate_script
super + [
init_command,
repository_command,
- script_command
+ repository_update_command,
+ preinstall_command,
+ install_command,
+ postinstall_command
].compact.join("\n")
end
@@ -30,17 +35,29 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
['helm', 'repo', 'add', name, repository].shelljoin if repository
end
- def script_command
+ def repository_update_command
+ 'helm repo update' if repository
+ end
+
+ def install_command
command = ['helm', 'install', chart] + install_command_flags
- command.shelljoin + " >/dev/null\n"
+ command.shelljoin
+ end
+
+ def preinstall_command
+ preinstall.join("\n") if preinstall
+ end
+
+ def postinstall_command
+ postinstall.join("\n") if postinstall
end
def install_command_flags
diff --git a/lib/gitlab/kubernetes/helm/upgrade_command.rb b/lib/gitlab/kubernetes/helm/upgrade_command.rb
index 74188046739..b36315f7a82 100644
--- a/lib/gitlab/kubernetes/helm/upgrade_command.rb
+++ b/lib/gitlab/kubernetes/helm/upgrade_command.rb
@@ -36,7 +36,7 @@ module Gitlab
private
def init_command
- 'helm init --client-only >/dev/null'
+ 'helm init --client-only'
end
def repository_command
@@ -50,7 +50,7 @@ module Gitlab
" --namespace #{::Gitlab::Kubernetes::Helm::NAMESPACE}" \
" -f /data/helm/#{name}/config/values.yaml"
- "helm upgrade #{name} #{chart}#{upgrade_flags} >/dev/null\n"
+ "helm upgrade #{name} #{chart}#{upgrade_flags}"
end
def optional_version_flag
diff --git a/lib/gitlab/private_commit_email.rb b/lib/gitlab/private_commit_email.rb
new file mode 100644
index 00000000000..bade2248ccd
--- /dev/null
+++ b/lib/gitlab/private_commit_email.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module PrivateCommitEmail
+ TOKEN = "_private".freeze
+
+ class << self
+ def regex
+ hostname_regexp = Regexp.escape(Gitlab::CurrentSettings.current_application_settings.commit_email_hostname)
+
+ /\A(?<id>([0-9]+))\-([^@]+)@#{hostname_regexp}\z/
+ end
+
+ def user_id_for_email(email)
+ match = email&.match(regex)
+ return unless match
+
+ match[:id].to_i
+ end
+
+ def for_user(user)
+ hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname
+
+ "#{user.id}-#{user.username}@#{hostname}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index 30c6806b68e..59f8dd889aa 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -29,7 +29,7 @@ module Gitlab
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
- def extract_commands(content)
+ def extract_commands(content, only: nil)
return [content, []] unless content
content = content.dup
@@ -37,7 +37,7 @@ module Gitlab
commands = []
content.delete!("\r")
- content.gsub!(commands_regex) do
+ content.gsub!(commands_regex(only: only)) do
if $~[:cmd]
commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
''
@@ -60,8 +60,8 @@ module Gitlab
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex
- names = command_names.map(&:to_s)
+ def commands_regex(only:)
+ names = command_names(limit_to_commands: only).map(&:to_s)
@commands_regex ||= %r{
(?<code>
@@ -133,10 +133,14 @@ module Gitlab
[content, commands]
end
- def command_names
+ def command_names(limit_to_commands:)
command_definitions.flat_map do |command|
next if command.noop?
+ if limit_to_commands && (command.all_names & limit_to_commands).empty?
+ next
+ end
+
command.all_names
end.compact
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cc0817bdcd2..069cd1f802a 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -62,6 +62,7 @@ module Gitlab
clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed),
clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed),
clusters_applications_runner: count(::Clusters::Applications::Runner.installed),
+ clusters_applications_knative: count(::Clusters::Applications::Knative.installed),
in_review_folder: count(::Environment.in_review_folder),
groups: count(Group),
issues: count(Issue),
@@ -126,7 +127,6 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
types = {
- JiraService: :projects_jira_active,
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
PrometheusService: :projects_prometheus_active
@@ -134,6 +134,23 @@ module Gitlab
results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1))
types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 }
+ .merge(jira_usage)
+ end
+
+ def jira_usage
+ # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
+ # so we can just check for subdomains of atlassian.net
+ services = count(
+ Service.unscoped.where(type: :JiraService, active: true)
+ .group("CASE WHEN properties LIKE '%.atlassian.net%' THEN 'cloud' ELSE 'server' END"),
+ fallback: Hash.new(-1)
+ )
+
+ {
+ projects_jira_server_active: services['server'] || 0,
+ projects_jira_cloud_active: services['cloud'] || 0,
+ projects_jira_active: services['server'] == -1 ? -1 : services.values.sum
+ }
end
def count(relation, fallback: -1)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 45fc072900a..d3e1a51370e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -103,6 +103,9 @@ msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
+msgid "%{count} more assignees"
+msgstr ""
+
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
@@ -264,6 +267,9 @@ msgstr ""
msgid "A deleted user"
msgstr ""
+msgid "A member of GitLab's abuse team will review your report as soon as possible."
+msgstr ""
+
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr ""
@@ -336,6 +342,9 @@ msgstr ""
msgid "Add a table"
msgstr ""
+msgid "Add image comment"
+msgstr ""
+
msgid "Add license"
msgstr ""
@@ -1346,12 +1355,24 @@ msgstr ""
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
msgstr ""
+msgid "ClusterIntegration|A Knative build extends Kubernetes and utilizes existing Kubernetes primitives to provide you with the ability to run on-cluster container builds from source. For example, you can write a build that uses Kubernetes-native resources to obtain your source code from a repository, build it into container a image, and then run that image."
+msgstr ""
+
msgid "ClusterIntegration|API URL"
msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Add a Kubernetes cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
+msgstr ""
+
+msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects."
+msgstr ""
+
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
@@ -1454,6 +1475,9 @@ msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
+msgid "ClusterIntegration|Group cluster"
+msgstr ""
+
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
@@ -1496,13 +1520,16 @@ msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster"
+msgid "ClusterIntegration|Knative"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster details"
+msgid "ClusterIntegration|Knative Domain Name:"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster integration"
+msgid "ClusterIntegration|Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
@@ -1514,7 +1541,7 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details"
msgstr ""
-msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
msgid "ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project"
@@ -1523,10 +1550,13 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}."
+msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
+msgid "ClusterIntegration|Learn more about Kubernetes"
+msgstr ""
+
+msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Machine type"
@@ -1571,6 +1601,9 @@ msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
msgstr ""
+msgid "ClusterIntegration|Project cluster"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1661,9 +1694,6 @@ msgstr ""
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr ""
-msgid "ClusterIntegration|Toggle Kubernetes Cluster"
-msgstr ""
-
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
@@ -1718,12 +1748,18 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Comment"
+msgstr ""
+
msgid "Comment & resolve discussion"
msgstr ""
msgid "Comment & unresolve discussion"
msgstr ""
+msgid "Comment form position"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -1971,6 +2007,9 @@ msgstr ""
msgid "Copy file path to clipboard"
msgstr ""
+msgid "Copy link"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
@@ -2094,6 +2133,9 @@ msgstr ""
msgid "Custom CI config path"
msgstr ""
+msgid "Custom hostname (for private commit emails)"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -2190,6 +2232,9 @@ msgstr ""
msgid "Delete Snippet"
msgstr ""
+msgid "Delete comment"
+msgstr ""
+
msgid "Delete list"
msgstr ""
@@ -2726,6 +2771,9 @@ msgstr ""
msgid "Expiration date"
msgstr ""
+msgid "Explain the problem. If appropriate, provide a link to the relevant issue or comment."
+msgstr ""
+
msgid "Explore"
msgstr ""
@@ -3555,6 +3603,9 @@ msgstr ""
msgid "Last edited by %{name}"
msgstr ""
+msgid "Last reply by"
+msgstr ""
+
msgid "Last update"
msgstr ""
@@ -4156,6 +4207,9 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
+msgid "Notes|Collapse replies"
+msgstr ""
+
msgid "Notes|Show all activity"
msgstr ""
@@ -4590,6 +4644,9 @@ msgstr ""
msgid "Please try again"
msgstr ""
+msgid "Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately."
+msgstr ""
+
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
@@ -4701,6 +4758,9 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
+msgid "Profiles|Learn more"
+msgstr ""
+
msgid "Profiles|Made a private contribution"
msgstr ""
@@ -4743,6 +4803,9 @@ msgstr ""
msgid "Profiles|This email will be displayed on your public profile."
msgstr ""
+msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}"
+msgstr ""
+
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr ""
@@ -4767,6 +4830,9 @@ msgstr ""
msgid "Profiles|Upload new avatar"
msgstr ""
+msgid "Profiles|Use a private email - %{email}"
+msgstr ""
+
msgid "Profiles|Username change failed - %{message}"
msgstr ""
@@ -5159,6 +5225,9 @@ msgstr ""
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
+msgid "Report abuse to GitLab"
+msgstr ""
+
msgid "Reporting"
msgstr ""
@@ -6188,7 +6257,7 @@ msgstr ""
msgid "This is a confidential issue."
msgstr ""
-msgid "This is a delayed to run in "
+msgid "This is a delayed job to run in %{remainingTime}"
msgstr ""
msgid "This is the author's first Merge Request to this project."
@@ -6296,6 +6365,9 @@ msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
+msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
+msgstr ""
+
msgid "This source diff could not be displayed because it is too large."
msgstr ""
@@ -6320,6 +6392,9 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
+msgid "Time estimate"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -6534,6 +6609,9 @@ msgstr ""
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
+msgid "Today"
+msgstr ""
+
msgid "Todo"
msgstr ""
@@ -6567,6 +6645,9 @@ msgstr ""
msgid "Token"
msgstr ""
+msgid "Tomorrow"
+msgstr ""
+
msgid "Too many changes to show."
msgstr ""
@@ -7035,6 +7116,9 @@ msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
+msgid "Yesterday"
+msgstr ""
+
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -7502,6 +7586,11 @@ msgstr ""
msgid "remove due date"
msgstr ""
+msgid "reply"
+msgid_plural "replies"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "source"
msgstr ""
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 27a88534258..7f959441dac 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -109,6 +109,28 @@ module QA
known_hosts_file.close(true)
end
+ def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit')
+ self.git_protocol = version
+ add_file(file_name, file_content)
+ commit(commit_message)
+ push_changes
+
+ fetch_supported_git_protocol
+ end
+
+ def git_protocol=(value)
+ raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s)
+
+ run("git config protocol.version #{value}")
+ end
+
+ def fetch_supported_git_protocol
+ # ls-remote is one command known to respond to Git protocol v2 so we use
+ # it to get output including the version reported via Git tracing
+ output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
+ output[/git< version (\d+)/, 1] || 'unknown'
+ end
+
private
attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
@@ -117,8 +139,8 @@ module QA
!private_key_file.nil?
end
- def run(command_str)
- command = [env_vars, command_str, '2>&1'].compact.join(' ')
+ def run(command_str, *extra_env)
+ command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
Runtime::Logger.debug "Git: command=[#{command}]"
output, _ = Open3.capture2(command)
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index de18b9cefa6..23def93c7dd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -37,6 +37,11 @@ module QA
def select_comments_only_filter
click_element :discussion_filter
+ all_elements(:filter_options)[1].click
+ end
+
+ def select_history_only_filter
+ click_element :discussion_filter
all_elements(:filter_options).last.click
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index c7052a9f300..c4500f9be90 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -7,6 +7,16 @@ module QA
attr_writer :personal_access_token
+ # The environment variables used to indicate if the environment under test
+ # supports the given feature
+ SUPPORTED_FEATURES = {
+ git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2'
+ }.freeze
+
+ def supported_features
+ SUPPORTED_FEATURES
+ end
+
def debug?
enabled?(ENV['QA_DEBUG'], default: false)
end
@@ -104,6 +114,15 @@ module QA
raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
end
+ # Returns true if there is an environment variable that indicates that
+ # the feature is supported in the environment under test.
+ # All features are supported by default.
+ def can_test?(feature)
+ raise ArgumentError, %Q(Unknown feature "#{feature}") unless SUPPORTED_FEATURES.include? feature
+
+ enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true)
+ end
+
private
def enabled?(value, default: true)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index f5002c8032f..7145b950b6c 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -31,6 +31,7 @@ module QA
create_issue
Page::Project::Issue::Show.perform do |show|
+ show.select_all_activities_filter
show.comment('See attached banana for scale', attachment: file_to_attach)
show.refresh
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 83603f1cda7..ac34f72bb8f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -27,6 +27,11 @@ module QA
expect(show_page).to have_content("made the issue confidential")
expect(show_page).to have_content("My own comment")
+
+ show_page.select_history_only_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).not_to have_content("My own comment")
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
new file mode 100644
index 00000000000..43894372cf5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do
+ it 'user pushes to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ # Create a project to push to
+ project = Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_http_location.uri
+ repository.use_default_credentials
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
new file mode 100644
index 00000000000..bc88e6450f5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do
+ # Note: If you run this test against GDK make sure you've enabled sshd and
+ # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to
+ # `sshd_config`
+ # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
+
+ let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
+ let(:ssh_key) do
+ Factory::Resource::SSHKey.fabricate! do |resource|
+ resource.title = key_title
+ end
+ end
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ around do |example|
+ # Create an SSH key to be used with Git
+ login
+ ssh_key
+
+ example.run
+
+ # Remove the SSH key
+ login
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+ Page::Profile::SSHKeys.perform do |ssh_keys|
+ ssh_keys.remove_key(key_title)
+ end
+ end
+
+ it 'user pushes to the repository' do
+ # Create a project to push to
+ project = Factory::Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_ssh_location.uri
+
+ begin
+ repository.use_ssh_key(ssh_key)
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ ensure
+ repository.delete_ssh_key
+ end
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index ad397c13f0c..1bd8101c36d 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -25,6 +25,10 @@ module QA
args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+ QA::Runtime::Env.supported_features.each_key do |key|
+ args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key
+ end
+
args.push(options)
args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index c629f802aa4..faa154c78da 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -26,6 +26,36 @@ describe QA::Git::Repository do
end
end
+ describe '#git_protocol=' do
+ [0, 1, 2].each do |version|
+ it "configures git to use protocol version #{version}" do
+ expect(repository).to receive(:run).with("git config protocol.version #{version}")
+ repository.git_protocol = version
+ end
+ end
+
+ it 'raises an error if the version is unsupported' do
+ expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ end
+ end
+
+ describe '#fetch_supported_git_protocol' do
+ it "reports the detected version" do
+ expect(repository).to receive(:run).and_return("packet: git< version 2")
+ expect(repository.fetch_supported_git_protocol).to eq('2')
+ end
+
+ it 'reports unknown if version is unknown' do
+ expect(repository).to receive(:run).and_return("packet: git< version -1")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+
+ it 'reports unknown if content does not identify a version' do
+ expect(repository).to receive(:run).and_return("foo")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+ end
+
def cd_empty_temp_directory
tmp_dir = 'tmp/git-repository-spec/'
FileUtils.rm_rf(tmp_dir) if ::File.exist?(tmp_dir)
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index c59c415c148..ded51d5bb7c 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -3,49 +3,62 @@
describe QA::Runtime::Env do
include Support::StubENV
- shared_examples 'boolean method' do |method, env_key, default|
+ shared_examples 'boolean method' do |**kwargs|
+ it_behaves_like 'boolean method with parameter', kwargs
+ end
+
+ shared_examples 'boolean method with parameter' do |method:, param: nil, env_key:, default:|
context 'when there is an env variable set' do
it 'returns false when falsey values specified' do
stub_env(env_key, 'false')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, 'no')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, '0')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
end
it 'returns true when anything else specified' do
stub_env(env_key, 'true')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, '1')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, 'anything')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
end
end
context 'when there is no env variable set' do
it "returns the default, #{default}" do
stub_env(env_key, nil)
- expect(described_class.public_send(method)).to be(default)
+ expect(described_class.public_send(method, *param)).to be(default)
end
end
end
describe '.signup_disabled?' do
- it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
+ it_behaves_like 'boolean method',
+ method: :signup_disabled?,
+ env_key: 'SIGNUP_DISABLED',
+ default: false
end
describe '.debug?' do
- it_behaves_like 'boolean method', :debug?, 'QA_DEBUG', false
+ it_behaves_like 'boolean method',
+ method: :debug?,
+ env_key: 'QA_DEBUG',
+ default: false
end
describe '.chrome_headless?' do
- it_behaves_like 'boolean method', :chrome_headless?, 'CHROME_HEADLESS', true
+ it_behaves_like 'boolean method',
+ method: :chrome_headless?,
+ env_key: 'CHROME_HEADLESS',
+ default: true
end
describe '.running_in_ci?' do
@@ -182,4 +195,16 @@ describe QA::Runtime::Env do
expect(described_class.log_destination).to eq('path/to_file')
end
end
+
+ describe '.can_test?' do
+ it_behaves_like 'boolean method with parameter',
+ method: :can_test?,
+ param: :git_protocol_v2,
+ env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2',
+ default: true
+
+ it 'raises ArgumentError if feature is unknown' do
+ expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"')
+ end
+ end
end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index 9ddaf7ab1b3..741821ddf8c 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -76,6 +76,20 @@ describe QA::Specs::Runner do
end
end
+ context 'when git protocol v2 is not supported' do
+ before do
+ allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false)
+ end
+
+ subject { described_class.new }
+
+ it 'it includes default args and excludes the requires_git_protocol_v2 tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
def expect_rspec_runner_arguments(arguments)
expect(RSpec::Core::Runner).to receive(:run)
.with(arguments, $stderr, $stdout)
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index d372bcbdab1..016682e5a3d 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -126,6 +126,9 @@ function deploy() {
delete
cleanup
fi
+
+ create_secret
+
helm repo add gitlab https://charts.gitlab.io/
helm dep update .
diff --git a/scripts/trigger-build b/scripts/trigger-build
index dd0425b6472..873c41db456 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -107,7 +107,8 @@ module Trigger
{
'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'],
'ALTERNATIVE_SOURCES' => 'true',
- 'ee' => Trigger.ee? ? 'true' : 'false'
+ 'ee' => Trigger.ee? ? 'true' : 'false',
+ 'QA_BRANCH' => ENV['QA_BRANCH'] || 'master'
}
end
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 6d0483f0032..98946e4287b 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('entities/issue_boards')
+ expect(response).to match_response_schema('issues')
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('entities/issue_boards')
+ expect(response).to match_response_schema('issues')
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('entities/issue_board')
+ expect(response).to match_response_schema('issue')
end
end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 767fba7fd58..4f1f6bb31f3 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -28,8 +28,9 @@ describe SendFileUpload do
describe '#send_upload' do
let(:controller) { controller_class.new }
let(:temp_file) { Tempfile.new('test') }
+ let(:params) { {} }
- subject { controller.send_upload(uploader) }
+ subject { controller.send_upload(uploader, **params) }
before do
FileUtils.touch(temp_file)
@@ -52,7 +53,7 @@ describe SendFileUpload do
end
context 'with attachment' do
- let(:send_attachment) { controller.send_upload(uploader, attachment: 'test.js') }
+ let(:params) { { attachment: 'test.js' } }
it 'sends a file with content-type of text/plain' do
expected_params = {
@@ -62,7 +63,7 @@ describe SendFileUpload do
}
expect(controller).to receive(:send_file).with(uploader.path, expected_params)
- send_attachment
+ subject
end
context 'with a proxied file in object storage' do
@@ -83,7 +84,7 @@ describe SendFileUpload do
expect(controller).to receive(:headers) { headers }
expect(controller).to receive(:head).with(:ok)
- send_attachment
+ subject
end
end
end
@@ -95,11 +96,7 @@ describe SendFileUpload do
uploader.store!(temp_file)
end
- context 'and proxying is enabled' do
- before do
- allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
- end
-
+ shared_examples 'proxied file' do
it 'sends a file' do
headers = double
expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-disposition/)
@@ -115,6 +112,14 @@ describe SendFileUpload do
end
end
+ context 'and proxying is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
+ end
+
+ it_behaves_like 'proxied file'
+ end
+
context 'and proxying is disabled' do
before do
allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false }
@@ -125,6 +130,12 @@ describe SendFileUpload do
subject
end
+
+ context 'with proxy requested' do
+ let(:params) { { proxy: true } }
+
+ it_behaves_like 'proxied file'
+ end
end
end
end
diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..68a798542b6
--- /dev/null
+++ b/spec/controllers/groups/clusters/applications_controller_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ describe 'POST create' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { go }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { go }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ go
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:clusters_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ go
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(group_id: group)
+ end
+ end
+end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
new file mode 100644
index 00000000000..6e130f830a2
--- /dev/null
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -0,0 +1,574 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::ClustersController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ set(:group) { create(:group) }
+
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ def go(params = {})
+ get :index, params.reverse_merge(group_id: group)
+ end
+
+ context 'when feature flag is not enabled' do
+ before do
+ stub_feature_flags(group_clusters: false)
+ end
+
+ it 'renders 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(group_clusters: true)
+ end
+
+ describe 'functionality' do
+ context 'when group has one or more clusters' do
+ let(:group) { create(:group) }
+
+ let!(:enabled_cluster) do
+ create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
+ end
+
+ let!(:disabled_cluster) do
+ create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ it 'lists available clusters' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { group.clusters.page.total_pages }
+
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ end
+
+ it 'redirects to the page' do
+ go(page: last_page)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
+ end
+ end
+
+ context 'when group does not have a cluster' do
+ let(:group) { create(:group) }
+
+ it 'returns an empty state page' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index, partial: :empty_state)
+ expect(assigns(:clusters)).to eq([])
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET new' do
+ def go
+ get :new, group_id: group
+ end
+
+ describe 'functionality for new cluster' do
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'functionality for existing cluster' do
+ it 'has new object' do
+ go
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for new cluster' do
+ let(:legacy_abac_param) { 'true' }
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project-12345',
+ legacy_abac: legacy_abac_param
+ }
+ }
+ }
+ end
+
+ def go
+ post :create_gcp, params.merge(group_id: group)
+ end
+
+ describe 'functionality' do
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_gcp
+ expect(cluster).to be_kubernetes
+ expect(cluster.provider_gcp).to be_legacy_abac
+ end
+
+ context 'when legacy_abac param is false' do
+ let(:legacy_abac_param) { 'false' }
+
+ it 'creates a new cluster with legacy_abac_disabled' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(group.clusters.first.provider_gcp).not_to be_legacy_abac
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(described_class)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST create for existing cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test'
+ }
+ }
+ }
+ end
+
+ def go
+ post :create_user, params.merge(group_id: group)
+ end
+
+ describe 'functionality' do
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ end
+ end
+
+ context 'when creates a RBAC-enabled cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = group.clusters.first
+
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET cluster_status' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :cluster_status,
+ group_id: group.to_param,
+ id: cluster,
+ format: :json
+ end
+
+ describe 'functionality' do
+ it 'responds with matching schema' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+
+ it 'invokes schedule_status_update on each application' do
+ expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+
+ go
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET show' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ get :show,
+ group_id: group,
+ id: cluster
+ end
+
+ describe 'functionality' do
+ it 'renders view' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'PUT update' do
+ def go(format: :html)
+ put :update, params.merge(
+ group_id: group.to_param,
+ id: cluster,
+ format: format
+ )
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
+
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(group_cluster_path(group, cluster))
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ end
+
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name'
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ go(format: :json)
+
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: ''
+ }
+ }
+ end
+
+ it 'rejects changes' do
+ go(format: :json)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ def go
+ delete :destroy,
+ group_id: group,
+ id: cluster
+ end
+
+ describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
+
+ expect(response).to redirect_to(group_clusters_path(group))
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group]) }
+
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ context 'no group_id param' do
+ it 'does not respond to any action without group_id param' do
+ expect { get :index }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
+end
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index 5024ef71771..77060fdc3be 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -121,12 +121,19 @@ describe Import::BitbucketServerController do
@repo = double(slug: 'vim', project_key: 'asd', full_name: 'asd/vim', "valid?" => true, project_name: 'asd', browse_url: 'http://test', name: 'vim')
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo')
+ @created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing')
assign_session_tokens
end
it 'assigns repository categories' do
- created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_source: 'foo/bar', import_status: 'finished')
- expect(client).to receive(:repos).and_return([@repo, @invalid_repo])
+ created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_status: 'finished', import_source: @created_repo.browse_url)
+ repos = instance_double(BitbucketServer::Collection)
+
+ expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
+ expect(repos).to receive(:current_page).and_return(1)
+ expect(repos).to receive(:next_page).and_return(2)
+ expect(repos).to receive(:prev_page).and_return(nil)
+ expect(client).to receive(:repos).and_return(repos)
get :status
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 6091185e252..b3c8d6a954e 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -47,14 +47,37 @@ describe Projects::ArtifactsController do
context 'when codequality file type is supplied' do
let(:file_type) { 'codequality' }
- before do
- create(:ci_job_artifact, :codequality, job: job)
+ context 'when file is stored locally' do
+ before do
+ create(:ci_job_artifact, :codequality, job: job)
+ end
+
+ it 'sends the codequality report' do
+ expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
+
+ download_artifact(file_type: file_type)
+ end
end
- it 'sends the codequality report' do
- expect(controller).to receive(:send_file).with(job.job_artifacts_codequality.file.path, hash_including(disposition: 'attachment')).and_call_original
+ context 'when file is stored remotely' do
+ before do
+ stub_artifacts_object_storage
+ create(:ci_job_artifact, :remote_store, :codequality, job: job)
+ end
+
+ it 'sends the codequality report' do
+ expect(controller).to receive(:redirect_to).and_call_original
- download_artifact(file_type: file_type)
+ download_artifact(file_type: file_type)
+ end
+
+ context 'when proxied' do
+ it 'sends the codequality report' do
+ expect(Gitlab::Workhorse).to receive(:send_url).and_call_original
+
+ download_artifact(file_type: file_type, proxy: true)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 64b589a6d83..28f7e4634a5 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -331,10 +331,10 @@ describe Projects::BlobController do
expect(response).to redirect_to(
project_new_merge_request_path(
forked_project,
- merge_request_source_branch: "fork-test-1",
merge_request: {
source_project_id: forked_project.id,
target_project_id: project.id,
+ source_branch: "fork-test-1",
target_branch: "master"
}
)
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 04aece26590..483222363bb 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -122,7 +122,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -143,7 +143,7 @@ describe Projects::ClustersController do
it 'has new object' do
go
- expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::Cluster)
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
end
end
@@ -396,20 +396,6 @@ describe Projects::ClustersController do
end
describe 'PUT update' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
- }
- }
- end
-
def go(format: :html)
put :update, params.merge(namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -423,105 +409,73 @@ describe Projects::ClustersController do
stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
end
- context 'when cluster is provided by GCP' do
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- end
-
- it "does not change cluster name" do
- go
-
- cluster.reload
- expect(cluster.name).to eq('test-cluster')
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
- it "rejects changes" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- expect(cluster.enabled).to be_truthy
- end
- end
- end
-
- context 'when cluster is provided by user' do
- let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
}
}
- end
+ }
+ end
- it "updates and redirects back to show page" do
- go
+ it "updates and redirects back to show page" do
+ go
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, cluster))
- expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- expect(cluster.name).to eq('my-new-cluster-name')
- expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
- context 'when format is json' do
- context 'when changing parameters' do
- context 'when valid parameters are used' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
}
}
- end
+ }
+ end
- it "updates and redirects back to show page" do
- go(format: :json)
+ it "updates and redirects back to show page" do
+ go(format: :json)
- cluster.reload
- expect(response).to have_http_status(:no_content)
- expect(cluster.enabled).to be_falsey
- expect(cluster.name).to eq('my-new-cluster-name')
- expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
end
+ end
- context 'when invalid parameters are used' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- platform_kubernetes_attributes: {
- namespace: 'my invalid namespace #@'
- }
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ platform_kubernetes_attributes: {
+ namespace: 'my invalid namespace #@'
}
}
- end
+ }
+ end
- it "rejects changes" do
- go(format: :json)
+ it "rejects changes" do
+ go(format: :json)
- expect(response).to have_http_status(:bad_request)
- end
+ expect(response).to have_http_status(:bad_request)
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 3190f1ce9d4..ccd4fc4db3a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Projects::MilestonesController do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3c9ca22a051..ff65c76cf26 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -57,6 +57,11 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
+ factory :clusters_applications_knative, class: Clusters::Applications::Knative do
+ hostname 'example.com'
+ cluster factory: %i(cluster with_installed_helm provided_by_gcp)
+ end
+
factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
oauth_application factory: :oauth_application
cluster factory: %i(cluster with_installed_helm provided_by_gcp project)
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index 4a0d1b181ea..8169c457ab7 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -10,7 +10,7 @@ FactoryBot.define do
username 'xxxxxx'
password 'xxxxxx'
- after(:create) do |platform_kubernetes, evaluator|
+ before(:create) do |platform_kubernetes, evaluator|
pem_file = File.expand_path(Rails.root.join('spec/fixtures/clusters/sample_cert.pem'))
platform_kubernetes.ca_cert = File.read(pem_file)
end
diff --git a/spec/factories/merge_request_diff_files.rb b/spec/factories/merge_request_diff_files.rb
new file mode 100644
index 00000000000..469a7a0ac8d
--- /dev/null
+++ b/spec/factories/merge_request_diff_files.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_diff_file do
+ association :merge_request_diff
+
+ relative_order 0
+ new_file true
+ renamed_file false
+ deleted_file false
+ too_large false
+ a_mode 0
+ b_mode 100644
+ new_path 'foo'
+ old_path 'foo'
+ diff ''
+ binary false
+
+ trait :new_file do
+ relative_order 0
+ new_file true
+ renamed_file false
+ deleted_file false
+ too_large false
+ a_mode 0
+ b_mode 100644
+ new_path 'foo'
+ old_path 'foo'
+ diff ''
+ binary false
+ end
+
+ trait :renamed_file do
+ relative_order 662
+ new_file false
+ renamed_file true
+ deleted_file false
+ too_large false
+ a_mode 100644
+ b_mode 100644
+ new_path 'bar'
+ old_path 'baz'
+ diff ''
+ binary false
+ end
+ end
+end
diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb
new file mode 100644
index 00000000000..e7b51189538
--- /dev/null
+++ b/spec/factories/merge_request_diffs.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_diff do
+ association :merge_request
+ state :collected
+ commits_count 1
+
+ base_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ head_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ start_commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ end
+end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 0d4fd49bf3a..5be56a49903 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -46,6 +46,17 @@ FactoryBot.define do
)
end
+ factory :jira_cloud_service, class: JiraService do
+ project
+ active true
+ properties(
+ url: 'https://mysite.atlassian.net',
+ username: 'jira_user',
+ password: 'my-secret-password',
+ project_key: 'jira-key'
+ )
+ end
+
factory :hipchat_service do
project
type 'HipchatService'
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index eebc987499d..030993462b5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text with plural' do
page.within('.add-issues-modal') do
- all('.board-card .board-card-number').each do |el|
+ all('.board-card .js-board-card-number-container').each do |el|
el.click
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index ec0ca21450a..21779336559 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -78,7 +78,7 @@ describe 'Issue Boards', :js do
end
it 'moves from bottom to top' do
- drag(from_index: 2, to_index: 0)
+ drag(from_index: 2, to_index: 0, duration: 1020)
wait_for_requests
@@ -130,7 +130,7 @@ describe 'Issue Boards', :js do
end
it 'moves to bottom of another list' do
- drag(list_from_index: 1, list_to_index: 2, to_index: 2)
+ drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
wait_for_requests
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0a24c5e906a..975b7944741 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -147,12 +147,10 @@ describe 'Dashboard Projects' do
end
context 'last push widget', :use_clean_rails_memory_store_caching do
- let(:ref) { "feature" }
-
before do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event, ref: ref, action: :created)
+ create(:push_event_payload, event: event, ref: 'feature', action: :created)
Users::LastPushEventService.new(user).cache_last_push_event(event)
@@ -167,9 +165,9 @@ describe 'Dashboard Projects' do
end
expect(page).to have_selector('.merge-request-form')
- expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref)
+ expect(current_path).to eq project_new_merge_request_path(project)
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
- expect(find('input#merge_request_source_branch', visible: false).value).to eq ref
+ expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4d04b8043ec..d01fc04311a 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe 'Group' do
+ let(:user) { create(:admin) }
+
before do
- sign_in(create(:admin))
+ sign_in(user)
end
matcher :have_namespace_error_message do
@@ -16,6 +18,24 @@ describe 'Group' do
visit new_group_path
end
+ describe 'as a non-admin' do
+ let(:user) { create(:user) }
+
+ it 'creates a group and persists visibility radio selection', :js do
+ stub_application_setting(default_group_visibility: :private)
+
+ fill_in 'Group name', with: 'test-group'
+ find("input[name='group[visibility_level]'][value='#{Gitlab::VisibilityLevel::PUBLIC}']").click
+ click_button 'Create group'
+
+ group = Group.find_by(name: 'test-group')
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(current_path).to eq(group_path(group))
+ expect(page).to have_selector '.visibility-icon .fa-globe'
+ end
+ end
+
describe 'with space in group path' do
it 'renders new group form with validation errors' do
fill_in 'Group URL', with: 'space group'
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index a124c99ecc8..0ccab5b2fac 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -9,10 +9,10 @@ describe 'create a merge request, allowing commits from members who can merge to
def visit_new_merge_request
visit project_new_merge_request_path(
source_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
end
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index f0d38dc6a0c..d790bdc82ce 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -114,10 +114,9 @@ describe 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
indicator = find('.js-image-badge', match: :first)
- badge = find('.image-diff-avatar-link .badge', match: :first)
+ badge = find('.user-avatar-link .badge', match: :first)
expect(indicator).to have_content('1')
expect(badge).to have_content('1')
@@ -157,8 +156,7 @@ describe 'Merge request > User creates image diff notes', :js do
visit project_merge_request_path(project, merge_request)
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'render diff indicators within the image frame' do
+ it 'render diff indicators within the image frame' do
diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
wait_for_requests
@@ -200,7 +198,6 @@ describe 'Merge request > User creates image diff notes', :js do
def create_image_diff_note
find('.js-add-image-diff-note-button', match: :first).click
- page.all('.js-add-image-diff-note-button')[0].click
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
click_button 'Comment'
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index d3da8cc6752..b58c433bbfe 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 1)
end
end
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
-
- expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ first('.js-diff-comment-avatar img').hover
end
+
+ expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}"
end
it 'toggles comments when clicking avatar' do
@@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.notes_holder')
page.within find_line(position.line_code(project.repository)) do
- first('img.js-diff-comment-avatar').click
+ first('.js-diff-comment-avatar img').click
end
expect(page).to have_selector('.notes_holder')
@@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
wait_for_requests
page.within find_line(position.line_code(project.repository)) do
- expect(page).not_to have_selector('img.js-diff-comment-avatar')
+ expect(page).not_to have_selector('.js-diff-comment-avatar img')
end
end
@@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 2)
end
end
@@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
end
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 0e439c8cb2d..74290c0fff9 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
describe 'Merge request > User sees deployment widget', :js do
- describe 'when deployed to an environment' do
+ describe 'when merge request has associated environments' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
@@ -10,30 +10,74 @@ describe 'Merge request > User sees deployment widget', :js do
let(:ref) { merge_request.target_branch }
let(:sha) { project.commit(ref).id }
let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) }
- let(:build) { create(:ci_build, :success, pipeline: pipeline) }
- let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
let!(:manual) { }
before do
merge_request.update!(merge_commit_sha: sha)
project.add_user(user, role)
sign_in(user)
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
end
- it 'displays that the environment is deployed' do
- wait_for_requests
+ context 'when deployment succeeded' do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
- 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))
+ it 'displays that the environment is deployed' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ 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
+ end
+
+ context 'when deployment failed' do
+ let(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :failed, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the deployment failed' 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
+
+ context 'when deployment running' do
+ let(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :running, environment: environment, sha: sha, ref: ref, deployable: build) }
+
+ it 'displays that the running deployment' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_content("Deploying to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
+ end
+
+ context 'when deployment will happen' do
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, 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("Deploying to #{environment.name}")
+ expect(page).not_to have_css('.js-deploy-time')
+ end
end
context 'with stop action' do
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) }
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
before do
deployment.update!(on_stop: manual.name)
+ visit project_merge_request_path(project, merge_request)
wait_for_requests
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 d907ed4198c..582be101399 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -20,10 +20,10 @@ describe 'Merge request > User sees merge widget', :js do
before do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'feature',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'feature',
target_branch: 'master'
})
end
diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
index 6dfc819fe8a..92cc73ddf1f 100644
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
@@ -13,10 +13,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows a specific WIP hint' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'wip',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'wip',
target_branch: 'master'
})
@@ -32,10 +32,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows the regular WIP message' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 147544740dc..ae41cf90576 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -109,13 +109,13 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'populates source branch button' do
- visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' })
expect(find('.js-source-branch')).to have_content('fix')
end
it 'allows to change the diff view' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'fix' })
click_link 'Changes'
@@ -131,7 +131,7 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'does not allow non-existing branches' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
expect(page).to have_content('The form contains the following errors')
expect(page).to have_content('Source branch "non-exist-source" does not exist')
@@ -140,7 +140,7 @@ describe 'Merge request > User selects branches for new MR', :js do
context 'when a branch contains commits that both delete and add the same image' do
it 'renders the diff successfully' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
click_link "Changes"
@@ -165,8 +165,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'shows pipelines for a new merge request' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
- merge_request: { target_branch: 'master' })
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
page.within('.merge-request') do
click_link 'Pipelines'
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index 6e681185e1f..b81478a481f 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -144,7 +144,7 @@ describe 'Merge request > User uses quick actions', :js do
describe '/target_branch command in merge request' do
let(:another_project) { create(:project, :public, :repository) }
- let(:new_url_opts) { { merge_request_source_branch: 'feature' } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
another_project.add_maintainer(user)
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
index 8ecdec491b8..ec1153b7f7f 100644
--- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
@@ -65,7 +65,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
check 'merge_request[squash]'
click_on 'Submit merge request'
wait_for_requests
@@ -95,7 +95,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is not enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
click_on 'Submit merge request'
wait_for_requests
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index f13c35c00d3..a85e7333ba8 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -35,37 +35,6 @@ describe 'Clusters', :js do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end
- context 'inline update of cluster' do
- it 'user can update cluster' do
- expect(page).to have_selector('.js-project-feature-toggle')
- end
-
- context 'with successful request' do
- it 'user sees updated cluster' do
- expect do
- page.find('.js-project-feature-toggle').click
- wait_for_requests
- end.to change { cluster.reload.enabled }
-
- expect(page).not_to have_selector('.is-checked')
- expect(cluster.reload).not_to be_enabled
- end
- end
-
- context 'with failed request' do
- it 'user sees not update cluster and error message' do
- expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
- allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
-
- page.find('.js-project-feature-toggle').click
-
- expect(page).to have_content('Something went wrong on our end.')
- expect(page).to have_selector('.is-checked')
- expect(cluster.reload).to be_enabled
- end
- end
- end
-
context 'when user clicks on a cluster' do
before do
click_link cluster.name
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 056f4ee2e22..9772a7bacac 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -25,7 +25,7 @@ describe 'Environment' do
end
context 'without deployments' do
- it 'does show no deployments' do
+ it 'does not show deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
end
end
@@ -43,6 +43,45 @@ describe 'Environment' do
end
end
+ context 'when there is a successful deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+ end
+
+ context 'when there is a running deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :running, environment: environment, deployable: build)
+ end
+
+ it 'does not show deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'when there is a failed deployment' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ let(:deployment) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ it 'does not show deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
context 'with related deployable present' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index d0ddf69d574..89954d35f91 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -128,7 +128,7 @@ describe 'Environments page', :js do
end
end
- context 'when there are deployments' do
+ context 'when there are successful deployments' do
let(:project) { create(:project, :repository) }
let!(:deployment) do
@@ -328,6 +328,22 @@ describe 'Environments page', :js do
end
end
end
+
+ context 'when there is a failed deployment' do
+ let(:project) { create(:project, :repository) }
+
+ let!(:deployment) do
+ create(:deployment, :failed,
+ environment: environment,
+ sha: project.commit.id)
+ end
+
+ it 'does not show deployments' do
+ visit_environments(project)
+
+ expect(page).to have_content('No deployments yet')
+ end
+ end
end
it 'does have a new environment button' do
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 6f620dff82b..847b5f0860f 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -57,7 +57,7 @@ describe 'Projects > Files > User creates a directory', :js do
expect(page).to have_content('From new-feature into master')
expect(page).to have_content('Add new directory')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
end
end
@@ -80,7 +80,8 @@ describe 'Projects > Files > User creates a directory', :js do
click_button('Create directory')
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+
+ expect(current_path).to eq(project_new_merge_request_path(fork))
end
end
end
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index 14b5bd58bd1..a4f94b7a76d 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -144,7 +144,7 @@ describe 'Projects > Files > User creates files' do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -182,7 +182,7 @@ describe 'Projects > Files > User creates files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
end
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index faf11ee9dd8..614b11fa5c8 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -63,7 +63,7 @@ describe 'Projects > Files > User deletes files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index c6b2aaea906..9eb65ec159c 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -86,7 +86,7 @@ describe 'Projects > Files > User edits files', :js do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -155,7 +155,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 09feb315465..e3da28d73c3 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -78,7 +78,7 @@ describe 'Projects > Files > User replaces files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
click_link('Changes')
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 92df8303f46..af3fc528a20 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -36,7 +36,7 @@ describe 'Projects > Files > User uploads files' do
click_button('Upload file')
expect(page).to have_content('New commit message')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
find("a[data-action='diffs']", text: 'Changes').click
@@ -92,7 +92,7 @@ describe 'Projects > Files > User uploads files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
find("a[data-action='diffs']", text: 'Changes').click
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index cbb935abd53..a1323699969 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -595,7 +595,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows delayed job', :js do
- expect(page).to have_content('This is a delayed to run in')
+ expect(page).to have_content('This is a delayed job to run in')
expect(page).to have_content("This job will automatically run after it's timer finishes.")
expect(page).to have_link('Unschedule job')
end
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 4be31511ceb..69561b4d733 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -22,8 +22,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit url
@@ -77,8 +77,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(forked_project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit fork_url
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 1ae0bd988f2..dfeeb3040c6 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -4,16 +4,13 @@ describe SnippetsFinder do
include Gitlab::Allowable
using RSpec::Parameterized::TableSyntax
- context 'filter by visibility' do
- let!(:snippet1) { create(:personal_snippet, :private) }
- let!(:snippet2) { create(:personal_snippet, :internal) }
- let!(:snippet3) { create(:personal_snippet, :public) }
+ describe '#initialize' do
+ it 'raises ArgumentError when a project and author are given' do
+ user = build(:user)
+ project = build(:project)
- it "returns public snippets when visibility is PUBLIC" do
- snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute
-
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ expect { described_class.new(user, author: user, project: project) }
+ .to raise_error(ArgumentError)
end
end
@@ -66,21 +63,21 @@ describe SnippetsFinder do
end
it "returns internal snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute
+ snippets = described_class.new(user, author: user, scope: :are_internal).execute
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, author: user, scope: :are_private).execute
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute
+ snippets = described_class.new(user, author: user, scope: :are_public).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
@@ -98,6 +95,13 @@ describe SnippetsFinder do
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
+
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, author: user).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
end
context 'filter by project' do
@@ -126,21 +130,21 @@ describe SnippetsFinder do
end
it "returns public snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+ snippets = described_class.new(user, project: project1, scope: :are_public).execute
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+ snippets = described_class.new(user, project: project1, scope: :are_internal).execute
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, project: project1, scope: :are_private).execute
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
@@ -156,10 +160,17 @@ describe SnippetsFinder do
it "returns private snippets for project members" do
project1.add_developer(user)
- snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+ snippets = described_class.new(user, project: project1, scope: :are_private).execute
expect(snippets).to include(@snippet1)
end
+
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, project: project1).execute
+
+ expect(snippets).to include(@snippet1, @snippet2, @snippet3)
+ end
end
describe '#execute' do
@@ -184,4 +195,6 @@ describe SnippetsFinder do
end
end
end
+
+ it_behaves_like 'snippet visibility'
end
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index 8d821ebb843..3e252ddd13c 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -8,6 +8,7 @@
"due_date": { "type": "date" },
"project_id": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
+ "time_estimate": { "type": "integer" },
"weight": { "type": "integer" },
"project": {
"type": "object",
diff --git a/spec/fixtures/api/schemas/entities/issue_boards.json b/spec/fixtures/api/schemas/entities/issue_boards.json
deleted file mode 100644
index 0ac1d9468c8..00000000000
--- a/spec/fixtures/api/schemas/entities/issue_boards.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "type": "object",
- "required" : [
- "issues",
- "size"
- ],
- "properties" : {
- "issues": {
- "type": "array",
- "items": { "$ref": "issue_board.json" }
- },
- "size": { "type": "integer" }
- },
- "additionalProperties": false
-}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 4878df43d28..a83ec55cede 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,6 +13,7 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "time_estimate": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
"assignable_labels_endpoint": { "type": "string" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index f7adc4e0b91..6df27bf32b9 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -9,6 +9,32 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "merged_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "merged_at": { "type": ["date", "null"] },
+ "closed_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "closed_at": { "type": ["date", "null"] },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"target_branch": { "type": "string" },
diff --git a/spec/fixtures/emails/merge_request_multiple_patches.eml b/spec/fixtures/emails/merge_request_multiple_patches.eml
new file mode 100644
index 00000000000..311b99a525d
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_multiple_patches.eml
@@ -0,0 +1,181 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_conflicting_patch.eml b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
new file mode 100644
index 00000000000..ddfdfe9e24a
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
@@ -0,0 +1,45 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: feature
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This does not apply
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
new file mode 100644
index 00000000000..965658721cd
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
@@ -0,0 +1,44 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+/target_branch with-codeowners
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
diff --git a/spec/fixtures/emails/valid_merge_request_with_patch.eml b/spec/fixtures/emails/valid_merge_request_with_patch.eml
new file mode 100644
index 00000000000..143fa77d1fa
--- /dev/null
+++ b/spec/fixtures/emails/valid_merge_request_with_patch.eml
@@ -0,0 +1,151 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
new file mode 100644
index 00000000000..cc38682a0ab
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
@@ -0,0 +1,19 @@
+From 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+--
+2.19.1
diff --git a/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
new file mode 100644
index 00000000000..905002ae898
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
@@ -0,0 +1,23 @@
+From 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+--
+2.19.1
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 466e018d68c..8d0679e5699 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -2,18 +2,18 @@ require 'spec_helper'
describe EventsHelper do
describe '#event_commit_title' do
- let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
+ let(:message) { 'foo & bar ' + 'A' * 70 + '\n' + 'B' * 80 }
subject { helper.event_commit_title(message) }
- it "returns the first line, truncated to 70 chars" do
+ it 'returns the first line, truncated to 70 chars' do
is_expected.to eq(message[0..66] + "...")
end
- it "is not html-safe" do
+ it 'is not html-safe' do
is_expected.not_to be_a(ActiveSupport::SafeBuffer)
end
- it "handles empty strings" do
+ it 'handles empty strings' do
expect(helper.event_commit_title("")).to eq("")
end
@@ -22,7 +22,7 @@ describe EventsHelper do
end
it 'does not escape HTML entities' do
- expect(helper.event_commit_title("foo & bar")).to eq("foo & bar")
+ expect(helper.event_commit_title('foo & bar')).to eq('foo & bar')
end
end
@@ -30,38 +30,54 @@ describe EventsHelper do
let(:event) { create(:event) }
let(:project) { create(:project, :public, :repository) }
- it "returns project issue url" do
- event.target = create(:issue)
+ context 'issue' do
+ before do
+ event.target = create(:issue)
+ end
- expect(helper.event_feed_url(event)).to eq(project_issue_url(event.project, event.issue))
+ it 'returns the project issue url' do
+ expect(helper.event_feed_url(event)).to eq(project_issue_url(event.project, event.target))
+ end
+
+ it 'contains the project issue IID link' do
+ expect(helper.event_feed_title(event)).to include("##{event.target.iid}")
+ end
end
- it "returns project merge_request url" do
- event.target = create(:merge_request)
+ context 'merge request' do
+ before do
+ event.target = create(:merge_request)
+ end
+
+ it 'returns the project merge request url' do
+ expect(helper.event_feed_url(event)).to eq(project_merge_request_url(event.project, event.target))
+ end
- expect(helper.event_feed_url(event)).to eq(project_merge_request_url(event.project, event.merge_request))
+ it 'contains the project merge request IID link' do
+ expect(helper.event_feed_title(event)).to include("!#{event.target.iid}")
+ end
end
- it "returns project commit url" do
+ it 'returns project commit url' do
event.target = create(:note_on_commit, project: project)
expect(helper.event_feed_url(event)).to eq(project_commit_url(event.project, event.note_target))
end
- it "returns event note target url" do
+ it 'returns event note target url' do
event.target = create(:note)
expect(helper.event_feed_url(event)).to eq(event_note_target_url(event))
end
- it "returns project url" do
+ it 'returns project url' do
event.project = project
event.action = 1
expect(helper.event_feed_url(event)).to eq(project_url(event.project))
end
- it "returns push event feed url" do
+ it 'returns push event feed url' do
event = create(:push_event)
create(:push_event_payload, event: event, action: :pushed)
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index c1d0614c79e..9a2372de69f 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -1,6 +1,35 @@
require 'rails_helper'
describe ProfilesHelper do
+ describe '#commit_email_select_options' do
+ it 'returns an array with private commit email along with all the verified emails' do
+ user = create(:user)
+ private_email = user.private_commit_email
+
+ verified_emails = user.verified_emails - [private_email]
+ emails = [
+ ["Use a private email - #{private_email}", Gitlab::PrivateCommitEmail::TOKEN],
+ verified_emails
+ ]
+
+ expect(helper.commit_email_select_options(user)).to match_array(emails)
+ end
+ end
+
+ describe '#selected_commit_email' do
+ let(:user) { create(:user) }
+
+ it 'returns main email when commit email attribute is nil' do
+ expect(helper.selected_commit_email(user)).to eq(user.email)
+ end
+
+ it 'returns DB stored commit_email' do
+ user.update(commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
+ end
+ end
+
describe '#email_provider_label' do
it "returns nil for users without external email" do
user = create(:user)
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js
new file mode 100644
index 00000000000..9e49330c052
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_due_date_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import dateFormat from 'dateformat';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Due Date component', () => {
+ let vm;
+ let date;
+ const Component = Vue.extend(IssueDueDate);
+ const createComponent = (dueDate = new Date()) =>
+ mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) });
+
+ beforeEach(() => {
+ date = new Date();
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render "Today" if the due date is today', () => {
+ const timeContainer = vm.$el.querySelector('time');
+
+ expect(timeContainer.textContent.trim()).toEqual('Today');
+ });
+
+ it('should render "Yesterday" if the due date is yesterday', () => {
+ date.setDate(date.getDate() - 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday');
+ });
+
+ it('should render "Tomorrow" if the due date is one day from now', () => {
+ date.setDate(date.getDate() + 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow');
+ });
+
+ it('should render day of the week if due date is one week away', () => {
+ date.setDate(date.getDate() + 5);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
+ });
+
+ it('should render month and day for other dates', () => {
+ date.setDate(date.getDate() + 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(
+ dateFormat(date, 'mmm d', true),
+ );
+ });
+
+ it('should contain the correct `.text-danger` css class for overdue issue', () => {
+ date.setDate(date.getDate() - 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
new file mode 100644
index 00000000000..ba65d3287da
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Tine Estimate component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(IssueTimeEstimate);
+ vm = mountComponent(Component, {
+ estimate: 374460,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+ '2 weeks 3 days 1 minute',
+ );
+ });
+
+ it('prevents tooltip xss', done => {
+ const alertSpy = spyOn(window, 'alert');
+ vm.estimate = 'Foo <script>alert("XSS")</script>';
+
+ vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 58b7d45d913..6eda5047dd0 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -117,11 +117,9 @@ describe('Issue card component', () => {
});
it('sets title', () => {
- expect(
- component.$el
- .querySelector('.board-card-assignee img')
- .getAttribute('data-original-title'),
- ).toContain(`Assigned to ${user.name}`);
+ expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain(
+ `${user.name}`,
+ );
});
it('sets users path', () => {
@@ -154,7 +152,7 @@ describe('Issue card component', () => {
it('displays defaults avatar if users avatar is null', () => {
expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe(
- 'default_avatar?width=20',
+ 'default_avatar?width=24',
);
});
});
@@ -163,7 +161,6 @@ describe('Issue card component', () => {
describe('multiple assignees', () => {
beforeEach(done => {
component.issue.assignees = [
- user,
new ListAssignee({
id: 2,
name: 'user2',
@@ -187,11 +184,11 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
- it('renders all four assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
+ it('renders all three assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
});
- describe('more than four assignees', () => {
+ describe('more than three assignees', () => {
beforeEach(done => {
component.issue.assignees.push(
new ListAssignee({
@@ -207,12 +204,12 @@ describe('Issue card component', () => {
it('renders more avatar counter', () => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('+2');
});
- it('renders three assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
+ it('renders two assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2);
});
it('renders 99+ avatar counter', done => {
@@ -228,7 +225,7 @@ describe('Issue card component', () => {
Vue.nextTick(() => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('99+');
done();
});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index a70138c7eee..0e2cc13fa52 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -23,6 +23,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
+ knative: { title: 'Knative' },
},
});
});
@@ -46,6 +47,10 @@ describe('Applications', () => {
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
});
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -63,6 +68,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -86,6 +92,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -105,6 +112,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -123,6 +131,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -139,6 +148,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -155,6 +165,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ knative: { title: 'Knative', status: 'installed', hostname: '' },
},
});
@@ -171,6 +182,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ knative: { title: 'Knative' },
},
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 4e6ad11cd92..73abf6504c0 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -33,6 +33,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLING,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -67,6 +72,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLABLE,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -77,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
+ '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index e0f55a12fca..34ed36afa5b 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -100,6 +100,14 @@ describe('Clusters Store', () => {
requestReason: null,
hostname: '',
},
+ knative: {
+ title: 'Knative',
+ status: mockResponseData.applications[5].status,
+ statusReason: mockResponseData.applications[5].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
});
});
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index 67f7b569f47..36bd042f3c4 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -1,15 +1,24 @@
import Vue from 'vue';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import '~/behaviors/markdown/render_gfm';
import diffFileMockData from '../mock_data/diff_file';
+import discussionsMockData from '../mock_data/diff_discussions';
describe('DiffContent', () => {
const Component = Vue.extend(DiffContentComponent);
let vm;
beforeEach(() => {
+ const store = createStore();
+ store.state.notes.noteableData = {
+ current_user: {
+ can_create_note: false,
+ },
+ };
+
vm = mountComponentWithStore(Component, {
store,
props: {
@@ -46,21 +55,57 @@ describe('DiffContent', () => {
});
describe('image diff', () => {
- beforeEach(() => {
+ beforeEach(done => {
vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
vm.diffFile.newSha = 'DEF';
vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
vm.diffFile.oldSha = 'ABC';
vm.diffFile.viewPath = '';
+ vm.diffFile.discussions = [{ ...discussionsMockData }];
+ vm.$store.state.diffs.commentForms.push({
+ fileHash: vm.diffFile.fileHash,
+ x: 10,
+ y: 20,
+ width: 100,
+ height: 200,
+ });
+
+ vm.$nextTick(done);
});
- it('should have image diff view in place', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+ it('should have image diff view in place', () => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
- expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+ });
- done();
+ it('renders image diff overlay', () => {
+ expect(vm.$el.querySelector('.image-diff-overlay')).not.toBe(null);
+ });
+
+ it('renders diff file discussions', () => {
+ expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+
+ describe('handleSaveNote', () => {
+ it('dispatches handleSaveNote', () => {
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.handleSaveNote('test');
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/saveDiffDiscussion', {
+ note: 'test',
+ formData: {
+ noteableData: jasmine.anything(),
+ noteableType: jasmine.anything(),
+ diffFile: vm.diffFile,
+ positionType: 'image',
+ x: 10,
+ y: 20,
+ width: 100,
+ height: 200,
+ },
+ });
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 270f363825f..0bc9da5ad0f 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,24 +1,90 @@
import Vue from 'vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
-import store from '~/mr_notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
describe('DiffDiscussions', () => {
- let component;
+ let vm;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- beforeEach(() => {
- component = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
+ function createComponent(props = {}) {
+ const store = createStore();
+
+ vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
discussions: getDiscussionsMockData(),
+ ...props,
}).$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
});
describe('template', () => {
it('should have notes list', () => {
- const { $el } = component;
+ createComponent();
+
+ expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ });
+ });
+
+ describe('image commenting', () => {
+ it('renders collapsible discussion button', () => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
+ 'diff-notes-collapse',
+ );
+ });
+
+ it('dispatches toggleDiscussion when clicking collapse button', () => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-diff-notes-toggle').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ discussionId: vm.discussions[0].id,
+ });
+ });
+
+ it('renders expand button when discussion is collapsed', done => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ vm.discussions[0].expanded = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
+ expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
+ 'btn-transparent badge badge-pill',
+ );
+
+ done();
+ });
+ });
+
+ it('hides discussion when collapsed', done => {
+ createComponent({ shouldCollapseDiscussions: true });
+
+ vm.discussions[0].expanded = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('renders badge on avatar', () => {
+ createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
- expect($el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
+ expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
});
});
});
diff --git a/spec/javascripts/diffs/components/image_diff_overlay_spec.js b/spec/javascripts/diffs/components/image_diff_overlay_spec.js
new file mode 100644
index 00000000000..d76ab745fe1
--- /dev/null
+++ b/spec/javascripts/diffs/components/image_diff_overlay_spec.js
@@ -0,0 +1,146 @@
+import Vue from 'vue';
+import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
+import { createStore } from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { imageDiffDiscussions } from '../mock_data/diff_discussions';
+
+describe('Diffs image diff overlay component', () => {
+ const dimensions = {
+ width: 100,
+ height: 200,
+ };
+ let Component;
+ let vm;
+
+ function createComponent(props = {}, extendStore = () => {}) {
+ const store = createStore();
+
+ extendStore(store);
+
+ vm = createComponentWithStore(Component, store, {
+ discussions: [...imageDiffDiscussions],
+ fileHash: 'ABC',
+ ...props,
+ });
+ }
+
+ beforeAll(() => {
+ Component = Vue.extend(ImageDiffOverlay);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders comment badges', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(2);
+ });
+
+ it('renders index of discussion in badge', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].textContent.trim()).toBe('1');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].textContent.trim()).toBe('2');
+ });
+
+ it('renders icon when showCommentIcon is true', () => {
+ createComponent({ showCommentIcon: true });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.js-image-badge svg')).not.toBe(null);
+ });
+
+ it('sets badge comment positions', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.left).toBe('10px');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[0].style.top).toBe('10px');
+
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.left).toBe('5px');
+ expect(vm.$el.querySelectorAll('.js-image-badge')[1].style.top).toBe('5px');
+ });
+
+ it('renders single badge for discussion object', () => {
+ createComponent({
+ discussions: {
+ ...imageDiffDiscussions[0],
+ },
+ });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelectorAll('.js-image-badge').length).toBe(1);
+ });
+
+ it('dispatches openDiffFileCommentForm when clicking overlay', () => {
+ createComponent({ canComment: true });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-add-image-diff-note-button').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/openDiffFileCommentForm', {
+ fileHash: 'ABC',
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 200,
+ });
+ });
+
+ describe('toggle discussion', () => {
+ it('disables buttons when shouldToggleDiscussion is false', () => {
+ createComponent({ shouldToggleDiscussion: false });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ expect(vm.$el.querySelector('.js-image-badge').hasAttribute('disabled')).toBe(true);
+ });
+
+ it('dispatches toggleDiscussion when clicking image badge', () => {
+ createComponent();
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-image-badge').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { discussionId: '1' });
+ });
+ });
+
+ describe('comment form', () => {
+ beforeEach(() => {
+ createComponent({}, store => {
+ store.state.diffs.commentForms.push({
+ fileHash: 'ABC',
+ x: 20,
+ y: 10,
+ });
+ });
+ spyOn(vm, 'getImageDimensions').and.returnValue(dimensions);
+ vm.$mount();
+ });
+
+ it('renders comment form badge', () => {
+ expect(vm.$el.querySelector('.comment-indicator')).not.toBe(null);
+ });
+
+ it('sets comment form badge position', () => {
+ expect(vm.$el.querySelector('.comment-indicator').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.comment-indicator').style.top).toBe('10px');
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index 0ad214ea4a4..5ffe5a366ba 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -492,3 +492,24 @@ export default {
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 = [
+ {
+ id: '1',
+ position: {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 200,
+ },
+ },
+ {
+ id: '2',
+ position: {
+ x: 5,
+ y: 5,
+ width: 100,
+ height: 200,
+ },
+ },
+];
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index d7bc0dbe431..be194ab414f 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -237,4 +237,5 @@ export default {
},
},
],
+ discussions: [],
};
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index bb623953710..17d0f31bdd3 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -218,6 +218,7 @@ describe('DiffsStoreActions', () => {
],
};
const singleDiscussion = {
+ id: '1',
fileHash: 'ABC',
line_code: 'ABC_1_1',
};
@@ -230,6 +231,7 @@ describe('DiffsStoreActions', () => {
{
type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE,
payload: {
+ id: '1',
fileHash: 'ABC',
lineCode: 'ABC_1_1',
},
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 807a9e3baf0..9c3a38fd526 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -49,17 +49,17 @@ describe('Diffs Module Getters', () => {
});
});
- describe('areAllFilesCollapsed', () => {
+ describe('hasCollapsedFile', () => {
it('returns true when all files are collapsed', () => {
localState.diffFiles = [{ collapsed: true }, { collapsed: true }];
- expect(getters.areAllFilesCollapsed(localState)).toEqual(true);
+ expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
- it('returns false when at least one file is not collapsed', () => {
+ it('returns true when at least one file is collapsed', () => {
localState.diffFiles = [{ collapsed: false }, { collapsed: true }];
- expect(getters.areAllFilesCollapsed(localState)).toEqual(false);
+ expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
});
diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb
index 6d5c6d5334f..82d7a5e394e 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -5,16 +5,24 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project_empty_repo, namespace: namespace, path: 'builds-project') }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') }
+ let!(:delayed_job) do
+ create(:ci_build, :scheduled,
+ pipeline: pipeline,
+ name: 'delayed job',
+ stage: 'test',
+ commands: 'test')
+ end
render_views
before(:all) do
clean_frontend_fixtures('builds/')
+ clean_frontend_fixtures('jobs/')
end
before do
@@ -34,4 +42,15 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
+
+ it 'jobs/delayed.json' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: delayed_job.to_param,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
end
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index f8ca43fc150..fcf3780f0ea 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -8,6 +8,7 @@ import { resetStore } from '../store/helpers';
import job from '../mock_data';
describe('Job App ', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const Component = Vue.extend(jobApp);
let store;
let vm;
@@ -101,7 +102,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 triggered 1 year ago by Root');
+ ).toContain('passed Job #4757 triggered 1 year ago by Root');
done();
}, 0);
});
@@ -127,7 +128,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 created 3 weeks ago by Root');
+ ).toContain('passed Job #4757 created 3 weeks ago by Root');
done();
}, 0);
});
@@ -420,6 +421,36 @@ describe('Job App ', () => {
done();
}, 0);
});
+
+ it('displays remaining time for a delayed job', done => {
+ const oneHourInMilliseconds = 3600000;
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds,
+ );
+ mock.onGet(props.endpoint).replyOnce(200, { ...delayedJobFixture });
+
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ store.subscribeAction(action => {
+ if (action.type !== 'receiveJobSuccess') {
+ return;
+ }
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-job-empty-state')).not.toBeNull();
+
+ const title = vm.$el.querySelector('.js-job-empty-state-title');
+
+ expect(title).toContainText('01:00:00');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
});
diff --git a/spec/javascripts/jobs/components/job_container_item_spec.js b/spec/javascripts/jobs/components/job_container_item_spec.js
index 8588eda19c8..2d108f1ad7f 100644
--- a/spec/javascripts/jobs/components/job_container_item_spec.js
+++ b/spec/javascripts/jobs/components/job_container_item_spec.js
@@ -4,6 +4,7 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
import job from '../mock_data';
describe('JobContainerItem', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const Component = Vue.extend(JobContainerItem);
let vm;
@@ -70,4 +71,29 @@ describe('JobContainerItem', () => {
expect(vm.$el).toHaveSpriteIcon('retry');
});
});
+
+ describe('for delayed job', () => {
+ beforeEach(() => {
+ const remainingMilliseconds = 1337000;
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds,
+ );
+ });
+
+ it('displays remaining time in tooltip', done => {
+ vm = mountComponent(Component, {
+ job: delayedJobFixture,
+ isActive: false,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual(
+ 'delayed job - delayed manual action (00:22:17)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
new file mode 100644
index 00000000000..48a6b80b365
--- /dev/null
+++ b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('DelayedJobMixin', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
+ const dummyComponent = Vue.extend({
+ mixins: [delayedJobMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ template: '<div>{{ remainingTime }}</div>',
+ });
+
+ let vm;
+
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jasmine.clock().uninstall();
+ });
+
+ describe('if job is empty object', () => {
+ beforeEach(() => {
+ vm = mountComponent(dummyComponent, {
+ job: {},
+ });
+ });
+
+ it('sets remaining time to 00:00:00', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+
+ describe('after mounting', () => {
+ beforeEach(done => {
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('doe not update remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+ });
+ });
+
+ describe('if job is delayed job', () => {
+ let remainingTimeInMilliseconds = 42000;
+
+ beforeEach(() => {
+ spyOn(Date, 'now').and.callFake(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
+ );
+ vm = mountComponent(dummyComponent, {
+ job: delayedJobFixture,
+ });
+ });
+
+ it('sets remaining time to 00:00:00', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+
+ describe('after mounting', () => {
+ beforeEach(done => {
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:42');
+ });
+
+ it('updates remaining time', done => {
+ remainingTimeInMilliseconds = 41000;
+ jasmine.clock().tick(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.innerText).toBe('00:00:41');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index d699e66b8ca..bebe76f76c5 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -336,6 +336,12 @@ describe('prettyTime methods', () => {
expect(timeString).toBe('0m');
});
+
+ it('should return non-condensed representation of time object', () => {
+ const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 };
+
+ expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
+ });
});
describe('abbreviateTime', () => {
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index 239d7950907..0c16103714a 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import createStore from '~/notes/stores';
+import { createStore } from '~/mr_notes/stores';
import { mountComponentWithStore } from 'spec/helpers';
const discussionFixture = 'merge_requests/diff_discussion.json';
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index d7298cb3483..f6c854e6def 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -55,7 +55,7 @@ describe('issue_note_actions component', () => {
expect(vm.$el.querySelector('.js-note-edit')).toBeDefined();
});
- it('should be possible to report as abuse', () => {
+ it('should be possible to report abuse to GitLab', () => {
expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined();
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index b447e79b0df..81cb3e1f74d 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
@@ -33,9 +34,20 @@ describe('noteable_discussion component', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
+ it('should not render discussion header for non diff discussions', () => {
+ expect(vm.$el.querySelector('.discussion-header')).toBeNull();
+ });
+
it('should render discussion header', () => {
- expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
- expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
+ const discussion = { ...discussionMock };
+ discussion.diff_file = mockDiffFile;
+ discussion.diff_discussion = true;
+ const diffDiscussionVm = new Component({
+ store,
+ propsData: { discussion },
+ }).$mount();
+
+ expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull();
});
describe('actions', () => {
diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
new file mode 100644
index 00000000000..2ead8cc6e6a
--- /dev/null
+++ b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
@@ -0,0 +1,78 @@
+import Vue from 'vue';
+import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { note } from '../mock_data';
+
+const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
+
+describe('toggle replies widget for notes', () => {
+ let vm;
+ let ToggleRepliesWidget;
+ const noteFromOtherUser = deepCloneObject(note);
+ noteFromOtherUser.author.username = 'fatihacet';
+
+ const noteFromAnotherUser = deepCloneObject(note);
+ noteFromAnotherUser.author.username = 'mgreiling';
+ noteFromAnotherUser.author.name = 'Mike Greiling';
+
+ const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser];
+
+ beforeEach(() => {
+ ToggleRepliesWidget = Vue.extend(toggleRepliesWidget);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: true,
+ });
+ });
+
+ it('should render the collapsed', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.classList.contains('collapsed')).toEqual(true);
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toEqual(3);
+ expect(vm.$el.querySelector('time')).not.toBeNull();
+ expect(vmTextContent).toContain('5 replies');
+ expect(vmTextContent).toContain(`Last reply by ${noteFromAnotherUser.author.name}`);
+ });
+
+ it('should emit toggle event when the replies text clicked', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-replies-text').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+
+ describe('expanded state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: false,
+ });
+ });
+
+ it('should render expanded state', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.querySelector('.collapse-replies-btn')).not.toBeNull();
+ expect(vmTextContent).toContain('Collapse replies');
+ });
+
+ it('should emit toggle event when the collapse replies text called', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-collapse-replies').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js
index 7cbcdc791e7..41b614cc95e 100644
--- a/spec/javascripts/pipelines/graph/job_item_spec.js
+++ b/spec/javascripts/pipelines/graph/job_item_spec.js
@@ -6,6 +6,7 @@ describe('pipeline graph job item', () => {
const JobComponent = Vue.extend(JobItem);
let component;
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const mockJob = {
id: 4256,
name: 'test',
@@ -167,4 +168,30 @@ describe('pipeline graph job item', () => {
expect(component.$el.querySelector(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 => {
+ 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);
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
index 473a062fc40..556a0976b29 100644
--- a/spec/javascripts/pipelines/header_component_spec.js
+++ b/spec/javascripts/pipelines/header_component_spec.js
@@ -51,7 +51,7 @@ describe('Pipeline details header', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
+ ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo');
});
describe('action buttons', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index c9011b403b7..d6c44f4c976 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => {
}).$mount();
const image = component.$el.querySelector('.js-pipeline-url-user img');
+ const tooltip = component.$el.querySelector(
+ '.js-pipeline-url-user .js-user-avatar-image-toolip',
+ );
expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual(
mockData.pipeline.user.web_url,
);
- expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
+ expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 506d01f5ec1..4c575536f0e 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => {
expect(
component.$el
- .querySelector('.table-section:nth-child(2) img')
- .getAttribute('data-original-title'),
+ .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toEqual(pipeline.user.name);
});
});
@@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => {
const commitAuthorLink = commitAuthorElement.getAttribute('href');
const commitAuthorName = commitAuthorElement
- .querySelector('img.avatar')
- .getAttribute('data-original-title');
+ .querySelector('.js-user-avatar-image-toolip')
+ .textContent.trim();
return { commitAuthorElement, commitAuthorLink, commitAuthorName };
};
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 97dacec1fce..18fcdf7ede1 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -98,8 +98,8 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
component.$el
- .querySelector('.commit-title .avatar-image-container img')
- .getAttribute('data-original-title'),
+ .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toContain(props.author.username);
expect(
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index e2c34508b0d..4da8c6196b1 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -47,7 +47,7 @@ describe('ContentViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
+ expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
index fcd231ec693..67a3a2e08bc 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
@@ -30,11 +30,11 @@ describe('DiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
`//raw/DEF/${RED_BOX_IMAGE_URL}`,
);
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
`//raw/ABC/${GREEN_BOX_IMAGE_URL}`,
);
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 380effdb669..2d3e178d249 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -52,13 +52,9 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
- GREEN_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
- RED_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
@@ -81,9 +77,7 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
- GREEN_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
@@ -97,9 +91,7 @@ describe('ImageDiffViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
- RED_BOX_IMAGE_URL,
- );
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
done();
});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 3bf497bc00b..7a741bdc067 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -73,7 +73,7 @@ describe('Header CI Component', () => {
});
it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
});
it('should render provided actions', () => {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index dc7652c77f7..5c4aa7cf844 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = {
size: 99,
@@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() {
});
it('should have <img> as a child element', function() {
- expect(vm.$el.tagName).toBe('IMG');
- expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
- });
-
- it('should properly compute tooltipContainer', function() {
- expect(vm.tooltipContainer).toBe('body');
- });
+ const imageElement = vm.$el.querySelector('img');
- it('should properly render tooltipContainer', function() {
- expect(vm.$el.getAttribute('data-container')).toBe('body');
+ expect(imageElement).not.toBe(null);
+ expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
});
it('should properly compute avatarSizeClass', function() {
@@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() {
});
it('should properly render img css', function() {
- const { classList } = vm.$el;
+ const { classList } = vm.$el.querySelector('img');
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() {
});
it('should add lazy attributes', function() {
- const { classList } = vm.$el;
- const lazyClass = classList.contains('lazy');
+ const imageElement = vm.$el.querySelector('img');
+ const lazyClass = imageElement.classList.contains('lazy');
expect(lazyClass).toBe(true);
- expect(vm.$el.getAttribute('src')).toBe(placeholderImage);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('src')).toBe(placeholderImage);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ });
+ });
+
+ describe('dynamic tooltip content', () => {
+ const props = DEFAULT_PROPS;
+ const slots = {
+ default: ['Action!'],
+ };
+
+ beforeEach(() => {
+ vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount();
+ });
+
+ it('renders the tooltip slot', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null);
+ });
+
+ it('renders the tooltip content', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain(
+ slots.default[0],
+ );
+ });
+
+ it('does not render tooltip data attributes for on avatar image', () => {
+ const avatarImg = vm.$el.querySelector('img');
+
+ expect(avatarImg.dataset.originalTitle).not.toBeDefined();
+ expect(avatarImg.dataset.placement).not.toBeDefined();
+ expect(avatarImg.dataset.container).not.toBeDefined();
});
});
});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index e022245d3ea..0151ad23ba2 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() {
it('should only render image tag in link', function() {
const childElements = this.userAvatarLink.$el.childNodes;
- expect(childElements[0].tagName).toBe('IMG');
+ expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null');
// Vue will render the hidden component as <!---->
expect(childElements[1].tagName).toBeUndefined();
});
it('should render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(this.userAvatarLink.shouldShowUsername).toBe(false);
+ expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText);
});
});
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
+ ).toEqual('');
});
it('should render username prop in <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(
- this.propsData.username,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(),
+ ).toEqual(this.propsData.username);
});
it('should render text tooltip for <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset
+ .originalTitle,
+ ).toEqual(this.propsData.tooltipText);
});
it('should render text tooltip placement for <span>', function() {
expect(
- this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'),
+ this.userAvatarLink.$el
+ .querySelector('.js-user-avatar-link-username')
+ .getAttribute('tooltip-placement'),
).toEqual(this.propsData.tooltipPlacement);
});
});
diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb
index f926ae963a4..5de0a9a65b5 100644
--- a/spec/lib/bitbucket_server/client_spec.rb
+++ b/spec/lib/bitbucket_server/client_spec.rb
@@ -13,7 +13,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests?state=ALL" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request, page_offset: 0, limit: nil)
subject.pull_requests(project, repo_slug)
end
@@ -29,7 +29,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests/1/activities" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity, page_offset: 0, limit: nil)
subject.activities(project, repo_slug, 1)
end
@@ -52,10 +52,16 @@ describe BitbucketServer::Client do
let(:path) { "/repos" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 0, limit: nil)
subject.repos
end
+
+ it 'requests a collection with an offset and limit' do
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 10, limit: 25)
+
+ subject.repos(page_offset: 10, limit: 25)
+ end
end
describe '#create_branch' do
diff --git a/spec/lib/bitbucket_server/collection_spec.rb b/spec/lib/bitbucket_server/collection_spec.rb
new file mode 100644
index 00000000000..ddd02bac88a
--- /dev/null
+++ b/spec/lib/bitbucket_server/collection_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BitbucketServer::Collection do
+ let(:connection) { instance_double(BitbucketServer::Connection) }
+ let(:page) { 1 }
+ let(:paginator) { BitbucketServer::Paginator.new(connection, 'http://more-data', :pull_request, page_offset: page) }
+
+ subject { described_class.new(paginator) }
+
+ describe '#current_page' do
+ it 'returns 1' do
+ expect(subject.current_page).to eq(1)
+ end
+ end
+
+ describe '#prev_page' do
+ it 'returns nil' do
+ expect(subject.prev_page).to be_nil
+ end
+ end
+
+ describe '#next_page' do
+ it 'returns 2' do
+ expect(subject.next_page).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/bitbucket_server/paginator_spec.rb b/spec/lib/bitbucket_server/paginator_spec.rb
index 2de50eba3c4..d268d4f23cf 100644
--- a/spec/lib/bitbucket_server/paginator_spec.rb
+++ b/spec/lib/bitbucket_server/paginator_spec.rb
@@ -20,6 +20,16 @@ describe BitbucketServer::Paginator do
expect { paginator.items }.to raise_error(StopIteration)
end
+ it 'obeys limits' do
+ limited = described_class.new(connection, 'http://more-data', :pull_request, page_offset: 0, limit: 1)
+ allow(limited).to receive(:fetch_next_page).and_return(first_page)
+
+ expect(limited.has_next_page?).to be_truthy
+ expect(limited.items).to match(['item_1'])
+ expect(limited.has_next_page?).to be_truthy
+ expect { limited.items }.to raise_error(StopIteration)
+ end
+
it 'calls the connection with different offsets' do
expect(connection).to receive(:get).with('http://more-data', start: 0, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return(page_attrs)
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1169938b80c..57d4577a90c 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -1,5 +1,4 @@
-require 'fast_spec_helper'
-require_dependency 'active_model'
+require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
@@ -11,7 +10,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
image services only except variables artifacts
- environment coverage]
+ environment coverage retry]
end
it { is_expected.to match_array result }
@@ -99,41 +98,42 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
- context 'when retry value is not correct' do
+ context 'when parallel value is not correct' do
context 'when it is not a numeric value' do
- let(:config) { { retry: true } }
+ let(:config) { { parallel: true } }
it 'returns error about invalid type' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry is not a number'
+ expect(entry.errors).to include 'job parallel is not a number'
end
end
- context 'when it is lower than zero' do
- let(:config) { { retry: -1 } }
+ context 'when it is lower than two' do
+ let(:config) { { parallel: 1 } }
it 'returns error about value too low' do
expect(entry).not_to be_valid
expect(entry.errors)
- .to include 'job retry must be greater than or equal to 0'
+ .to include 'job parallel must be greater than or equal to 2'
end
end
- context 'when it is not an integer' do
- let(:config) { { retry: 1.5 } }
+ context 'when it is bigger than 50' do
+ let(:config) { { parallel: 51 } }
- it 'returns error about wrong value' do
+ it 'returns error about value too high' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be an integer'
+ expect(entry.errors)
+ .to include 'job parallel must be less than or equal to 50'
end
end
- context 'when the value is too high' do
- let(:config) { { retry: 10 } }
+ context 'when it is not an integer' do
+ let(:config) { { parallel: 1.5 } }
- it 'returns error about value too high' do
+ it 'returns error about wrong value' do
expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be less than or equal to 2'
+ expect(entry.errors).to include 'job parallel must be an integer'
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
new file mode 100644
index 00000000000..164a9ed4c3d
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
@@ -0,0 +1,236 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Retry do
+ let(:entry) { described_class.new(config) }
+
+ shared_context 'when retry value is a numeric', :numeric do
+ let(:config) { max }
+ let(:max) {}
+ end
+
+ shared_context 'when retry value is a hash', :hash do
+ let(:config) { { max: max, when: public_send(:when) }.compact }
+ let(:when) {}
+ let(:max) {}
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ context 'when retry value is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is returned as a hash with max key' do
+ expect(value).to eq(max: 2)
+ end
+ end
+
+ context 'when retry value is a hash', :hash do
+ context 'and `when` is a string' do
+ let(:when) { 'unknown_failure' }
+
+ it 'returns when wrapped in an array' do
+ expect(value).to eq(when: ['unknown_failure'])
+ end
+ end
+
+ context 'and `when` is an array' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'returns when as it was passed' do
+ expect(value).to eq(when: %w[unknown_failure runner_system_failure])
+ end
+ end
+ end
+ end
+
+ describe 'validation' do
+ context 'when retry value is correct' do
+ context 'when it is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with max' do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when' do
+ let(:when) { 'unknown_failure' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when always' do
+ let(:when) { 'always' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with array when' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ # Those values are documented at `doc/ci/yaml/README.md`. If any of
+ # those values gets invalid, documentation must be updated. To make
+ # sure this is catched, check explicitly that all of the documented
+ # values are valid. If they are not it means the documentation and this
+ # array must be updated.
+ RETRY_WHEN_IN_DOCUMENTATION = %w[
+ always
+ unknown_failure
+ script_failure
+ api_failure
+ stuck_or_timeout_failure
+ runner_system_failure
+ missing_dependency_failure
+ runner_unsupported
+ ].freeze
+
+ RETRY_WHEN_IN_DOCUMENTATION.each do |reason|
+ context "with when from documentation `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ ::Ci::Build.failure_reasons.each_key do |reason|
+ context "with when from CommitStatus.failure_reasons `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ context 'when retry value is not correct' do
+ context 'when it is not a numeric nor an array' do
+ let(:config) { true }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when it is a numeric', :numeric do
+ context 'when it is lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config must be greater than or equal to 0'
+ end
+ end
+
+ context 'when it is not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when the value is too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config must be less than or equal to 2'
+ end
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with unknown keys' do
+ let(:config) { { max: 2, unknown_key: :something, one_more: :key } }
+
+ it 'returns error about the unknown key' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config contains unknown keys: unknown_key, one_more'
+ end
+ end
+
+ context 'with max lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry max must be greater than or equal to 0'
+ end
+ end
+
+ context 'with max not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be an integer'
+ end
+ end
+
+ context 'iwth max too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be less than or equal to 2'
+ end
+ end
+
+ context 'with when in wrong format' do
+ let(:when) { true }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when should be an array of strings or a string'
+ end
+ end
+
+ context 'with an unknown when string' do
+ let(:when) { 'unknown_reason' }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when is not included in the list'
+ end
+ end
+
+ context 'with an unknown failure reason in a when array' do
+ let(:when) { %w[unknown_reason runner_system_failure] }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when contains unknown values: unknown_reason'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb
new file mode 100644
index 00000000000..97926695b6e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::Normalizer do
+ let(:job_name) { :rspec }
+ let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } }
+ let(:config) { { job_name => job_config } }
+
+ describe '.normalize_jobs' do
+ subject { described_class.new(config).normalize_jobs }
+
+ it 'does not have original job' do
+ is_expected.not_to include(job_name)
+ end
+
+ it 'has parallelized jobs' do
+ job_names = [:"rspec 1/5", :"rspec 2/5", :"rspec 3/5", :"rspec 4/5", :"rspec 5/5"]
+
+ is_expected.to include(*job_names)
+ end
+
+ it 'sets job instance in options' do
+ expect(subject.values).to all(include(:instance))
+ end
+
+ it 'parallelizes jobs with original config' do
+ original_config = config[job_name].except(:name)
+ configs = subject.values.map { |config| config.except(:name, :instance) }
+
+ expect(configs).to all(eq(original_config))
+ end
+
+ context 'when the job is not parallelized' do
+ let(:job_config) { { script: 'rspec', name: 'rspec' } }
+
+ it 'returns the same hash' do
+ is_expected.to eq(config)
+ end
+ end
+
+ context 'when there is a job with a slash in it' do
+ let(:job_name) { :"rspec 35/2" }
+
+ it 'properly parallelizes job names' do
+ job_names = [:"rspec 35/2 1/5", :"rspec 35/2 2/5", :"rspec 35/2 3/5", :"rspec 35/2 4/5", :"rspec 35/2 5/5"]
+
+ is_expected.to include(*job_names)
+ end
+ end
+
+ context 'when jobs depend on parallelized jobs' do
+ let(:config) { { job_name => job_config, other_job: { script: 'echo 1', dependencies: [job_name.to_s] } } }
+
+ it 'parallelizes dependencies' do
+ job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"]
+
+ expect(subject[:other_job][:dependencies]).to include(*job_names)
+ end
+
+ it 'does not include original job name in dependencies' do
+ expect(subject[:other_job][:dependencies]).not_to include(job_name)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/scheduled_spec.rb b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
index 4a52b3ab8de..68b87fea75d 100644
--- a/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/scheduled_spec.rb
@@ -13,24 +13,10 @@ describe Gitlab::Ci::Status::Build::Scheduled do
end
describe '#status_tooltip' do
- context 'when scheduled_at is not expired' do
- let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
-
- it 'shows execute_in of the scheduled job' do
- Timecop.freeze(Time.now.change(usec: 0)) do
- expect(subject.status_tooltip).to include('00:01:00')
- end
- end
- end
-
- context 'when scheduled_at is expired' do
- let(:build) { create(:ci_build, :expired_scheduled, project: project) }
+ let(:build) { create(:ci_build, scheduled_at: 1.minute.since, project: project) }
- it 'shows 00:00' do
- Timecop.freeze do
- expect(subject.status_tooltip).to include('00:00')
- end
- end
+ it 'has a placeholder for the remaining time' do
+ expect(subject.status_tooltip).to include('%{remainingTime}')
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 85b23edce9f..441e8214181 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -53,11 +53,11 @@ module Gitlab
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
- YAML.dump(rspec: { script: 'rspec', retry: 1 })
+ YAML.dump(rspec: { script: 'rspec', retry: { max: 1 } })
end
it 'includes retry count in build options attribute' do
- expect(subject[:options]).to include(retry: 1)
+ expect(subject[:options]).to include(retry: { max: 1 })
end
end
@@ -645,6 +645,33 @@ module Gitlab
end
end
+ describe 'Parallel' do
+ context 'when job is parallelized' do
+ let(:parallel) { 5 }
+
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ parallel: parallel })
+ end
+
+ it 'returns parallelized jobs' do
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes('test')
+ build_options = builds.map { |build| build[:options] }
+
+ expect(builds.size).to eq(5)
+ expect(build_options).to all(include(:instance, parallel: parallel))
+ end
+
+ it 'does not have the original job' do
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes('test')
+
+ expect(builds).not_to include(:rspec)
+ end
+ end
+ end
+
describe 'cache' do
context 'when cache definition has unknown keys' do
it 'raises relevant validation error' do
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index ace3104f36f..f276f1a8ddf 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -93,5 +93,74 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
end
end
end
+
+ context 'when the email contains patch attachments' do
+ let(:email_raw) { fixture_file("emails/valid_merge_request_with_patch.eml") }
+
+ it 'creates the source branch and applies the patches' do
+ receiver.execute
+
+ branch = project.repository.find_branch('new-branch-with-a-patch')
+
+ expect(branch).not_to be_nil
+ expect(branch.dereferenced_target.message).to include('A commit from a patch')
+ end
+
+ it 'creates the merge request' do
+ expect { receiver.execute }
+ .to change { project.merge_requests.where(source_branch: 'new-branch-with-a-patch').size }.by(1)
+ end
+
+ it 'does not mention the patches in the created merge request' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.description).not_to include('0001-A-commit-from-a-patch.patch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:email_raw) { fixture_file("emails/merge_request_with_conflicting_patch.eml") }
+
+ it 'raises an error' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidAttachment)
+ end
+ end
+
+ context 'when specifying the target branch using quick actions' do
+ let(:email_raw) { fixture_file('emails/merge_request_with_patch_and_target_branch.eml') }
+
+ it 'creates the merge request with the correct target branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+
+ it 'based the merge request of the target_branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.diff_base_commit).to eq(project.repository.commit('with-codeowners'))
+ end
+ end
+ end
+ end
+
+ describe '#patch_attachments' do
+ let(:email_raw) { fixture_file('emails/merge_request_multiple_patches.eml') }
+ let(:mail) { Mail::Message.new(email_raw) }
+ subject(:handler) { described_class.new(mail, mail_key) }
+
+ it 'orders attachments ending in `.patch` by name' do
+ expected_filenames = ["0001-A-commit-from-a-patch.patch",
+ "0002-This-does-not-apply-to-the-feature-branch.patch"]
+
+ attachments = handler.__send__(:patch_attachments).map(&:filename)
+
+ expect(attachments).to eq(expected_filenames)
+ end
end
end
diff --git a/spec/lib/gitlab/git/patches/collection_spec.rb b/spec/lib/gitlab/git/patches/collection_spec.rb
new file mode 100644
index 00000000000..080be141c59
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/collection_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Collection do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content1) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch_content2) do
+ File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+ end
+
+ subject(:collection) { described_class.new([patch_content1, patch_content2]) }
+
+ describe '#size' do
+ it 'combines the size of the patches' do
+ expect(collection.size).to eq(549.bytes + 424.bytes)
+ end
+ end
+
+ describe '#valid_size?' do
+ it 'is not valid if the total size is bigger than 2MB' do
+ expect(collection).to receive(:size).and_return(2500.kilobytes)
+
+ expect(collection).not_to be_valid_size
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/commit_patches_spec.rb b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
new file mode 100644
index 00000000000..760112155ce
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::CommitPatches do
+ describe '#commit' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ Gitlab::Git::Patches::Collection.new([content_1, content_2])
+ end
+ let(:user) { build(:user) }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:repository) { create(:project, :repository).repository }
+
+ subject(:commit_patches) do
+ described_class.new(user, repository, branch_name, patches)
+ end
+
+ it 'applies the patches' do
+ new_rev = commit_patches.commit
+
+ expect(repository.commit(new_rev)).not_to be_nil
+ end
+
+ it 'updates the branch cache' do
+ expect(repository).to receive(:after_create_branch)
+
+ commit_patches.commit
+ end
+
+ context 'when the repository does not exist' do
+ let(:repository) { create(:project).repository }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'when the patch does not apply' do
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/patch_spec.rb b/spec/lib/gitlab/git/patches/patch_spec.rb
new file mode 100644
index 00000000000..7466e853b65
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/patch_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Patch do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch) { described_class.new(patch_content) }
+
+ describe '#size' do
+ it 'is correct' do
+ expect(patch.size).to eq(549.bytes)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index eaf64e3c9b4..b37fe2686b6 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -335,4 +335,37 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_commit_patches' do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n")
+ end
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'branch-with-patches' }
+
+ subject(:commit_patches) do
+ client.user_commit_patches(user, branch_name, patch_content)
+ end
+
+ it 'applies the patch correctly' do
+ branch_update = commit_patches
+
+ expect(branch_update).to be_branch_created
+
+ commit = repository.commit(branch_update.newrev)
+ expect(commit.author_email).to eq('patchuser@gitlab.org')
+ expect(commit.committer_email).to eq(user.email)
+ expect(commit.message.chomp).to eq('This does not apply to the `feature` branch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches }.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 65f073b2df3..87ab81d8169 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -23,15 +23,23 @@ describe 'Import/Export attribute configuration' do
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+ let(:ee_safe_attributes_file) { 'ee/spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:ee_safe_model_attributes) { File.exist?(ee_safe_attributes_file) ? YAML.load_file(ee_safe_attributes_file) : {} }
+
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
relation_attributes = relation_class.new.attributes.keys
- expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
-
current_attributes = parsed_attributes(relation_name, relation_attributes)
- safe_attributes = safe_model_attributes[relation_class.to_s]
+ safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
+
+ ee_safe_model_attributes[relation_class.to_s].to_a.each do |attribute|
+ safe_attributes << attribute
+ end
+
+ expect(safe_attributes).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
+
new_attributes = current_attributes - safe_attributes
expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
@@ -43,6 +51,7 @@ describe 'Import/Export attribute configuration' do
It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ #{"If the model/associations are EE-specific, use `#{File.expand_path(ee_safe_attributes_file)}`.\n" if ee_safe_model_attributes.any?}
Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
model in the +excluded_attributes+ section.
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index 72dc1817936..4a3b9d4bf6a 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
EOS
end
@@ -22,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller >/dev/null
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index f28941ce58f..2b7e3ea6def 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -5,6 +5,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:repository) { 'https://repository.example.com' }
let(:rbac) { false }
let(:version) { '1.2.3' }
+ let(:preinstall) { nil }
+ let(:postinstall) { nil }
let(:install_command) do
described_class.new(
@@ -13,7 +15,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
rbac: rbac,
files: files,
version: version,
- repository: repository
+ repository: repository,
+ preinstall: preinstall,
+ postinstall: postinstall
)
end
@@ -22,8 +26,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_comand}
EOS
end
@@ -38,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -49,8 +54,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -66,7 +72,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--version 1.2.3
--set rbac.create\\=true,rbac.enabled\\=true
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -78,7 +84,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
#{helm_install_command}
EOS
end
@@ -93,7 +99,53 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-key /data/helm/app-name/config/key.pem
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
+ EOS
+ end
+ end
+ end
+
+ context 'when there is a pre-install script' do
+ let(:preinstall) { ['/bin/date', '/bin/true'] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only
+ helm repo add app-name https://repository.example.com
+ helm repo update
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ /bin/date
+ /bin/true
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
+ EOS
+ end
+ end
+ end
+
+ context 'when there is a post-install script' do
+ let(:postinstall) { ['/bin/date', "/bin/false\n"] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only
+ helm repo add app-name https://repository.example.com
+ helm repo update
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml
+ /bin/date
+ /bin/false
EOS
end
end
@@ -105,8 +157,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -117,7 +170,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--name app-name
--version 1.2.3
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
@@ -129,8 +182,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add app-name https://repository.example.com
+ helm repo update
#{helm_install_command}
EOS
end
@@ -144,7 +198,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
--tls-cert /data/helm/app-name/config/cert.pem
--tls-key /data/helm/app-name/config/key.pem
--namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml >/dev/null
+ -f /data/helm/app-name/config/values.yaml
EOS
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
index 3dabf04413e..9c9fc91ef3c 100644
--- a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb
@@ -21,8 +21,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -33,8 +33,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -56,9 +56,9 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
+ helm init --client-only
helm repo add #{application.name} #{application.repository}
- helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
@@ -70,8 +70,8 @@ describe Gitlab::Kubernetes::Helm::UpgradeCommand do
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
- helm init --client-only >/dev/null
- helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ helm init --client-only
+ helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml
EOS
end
end
diff --git a/spec/lib/gitlab/private_commit_email_spec.rb b/spec/lib/gitlab/private_commit_email_spec.rb
new file mode 100644
index 00000000000..bc86cd3842a
--- /dev/null
+++ b/spec/lib/gitlab/private_commit_email_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PrivateCommitEmail do
+ let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname }
+
+ context '.regex' do
+ subject { described_class.regex }
+
+ it { is_expected.to match("1-foo@#{hostname}") }
+ it { is_expected.not_to match("1-foo@#{hostname}.foo") }
+ it { is_expected.not_to match('1-foo@users.noreply.gitlab.com') }
+ it { is_expected.not_to match('foo-1@users.noreply.gitlab.com') }
+ it { is_expected.not_to match('foobar@gitlab.com') }
+ end
+
+ context '.user_id_for_email' do
+ let(:id) { 1 }
+
+ it 'parses user id from email' do
+ email = "#{id}-foo@#{hostname}"
+
+ expect(described_class.user_id_for_email(email)).to eq(id)
+ end
+
+ it 'returns nil on invalid commit email' do
+ email = "#{id}-foo@users.noreply.bar.com"
+
+ expect(described_class.user_id_for_email(email)).to be_nil
+ end
+ end
+
+ context '.for_user' do
+ it 'returns email in the format id-username@hostname' do
+ user = create(:user)
+
+ expect(described_class.for_user(user)).to eq("#{user.id}-#{user.username}@#{hostname}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index 0166f6c2ee0..873bb359d6e 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -272,5 +272,24 @@ describe Gitlab::QuickActions::Extractor do
expect(commands).to be_empty
expect(msg).to eq expected
end
+
+ it 'limits to passed commands when they are passed' do
+ msg = <<~MSG.strip
+ Hello, we should only extract the commands passed
+ /reopen
+ /labels hello world
+ /power
+ MSG
+ expected_msg = <<~EXPECTED.strip
+ Hello, we should only extract the commands passed
+ /power
+ EXPECTED
+ expected_commands = [['reopen'], ['labels', 'hello world']]
+
+ msg, commands = extractor.extract_commands(msg, only: [:open, :labels])
+
+ expect(commands).to eq(expected_commands)
+ expect(msg).to eq expected_msg
+ end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 69ee5ff4bcd..b212d2b05f2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::UsageData do
before do
create(:jira_service, project: projects[0])
create(:jira_service, project: projects[1])
+ create(:jira_cloud_service, project: projects[2])
create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
@@ -20,6 +21,7 @@ describe Gitlab::UsageData do
create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
end
subject { described_class.data }
@@ -81,6 +83,7 @@ describe Gitlab::UsageData do
clusters_applications_ingress
clusters_applications_prometheus
clusters_applications_runner
+ clusters_applications_knative
in_review_folder
groups
issues
@@ -95,6 +98,8 @@ describe Gitlab::UsageData do
projects
projects_imported_from_github
projects_jira_active
+ projects_jira_server_active
+ projects_jira_cloud_active
projects_slack_notifications_active
projects_slack_slash_active
projects_prometheus_active
@@ -114,7 +119,9 @@ describe Gitlab::UsageData do
expect(count_data[:projects]).to eq(3)
expect(count_data[:projects_prometheus_active]).to eq(1)
- expect(count_data[:projects_jira_active]).to eq(2)
+ expect(count_data[:projects_jira_active]).to eq(3)
+ expect(count_data[:projects_jira_server_active]).to eq(2)
+ expect(count_data[:projects_jira_cloud_active]).to eq(1)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
@@ -126,6 +133,7 @@ describe Gitlab::UsageData do
expect(count_data[:clusters_applications_ingress]).to eq(1)
expect(count_data[:clusters_applications_prometheus]).to eq(1)
expect(count_data[:clusters_applications_runner]).to eq(1)
+ expect(count_data[:clusters_applications_knative]).to eq(1)
end
it 'works when queries time out' do
diff --git a/spec/migrations/steal_fill_store_upload_spec.rb b/spec/migrations/steal_fill_store_upload_spec.rb
new file mode 100644
index 00000000000..ed809baf2b5
--- /dev/null
+++ b/spec/migrations/steal_fill_store_upload_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181105201455_steal_fill_store_upload.rb')
+
+describe StealFillStoreUpload, :migration do
+ let(:uploads) { table(:uploads) }
+
+ describe '#up' do
+ it 'steals the FillStoreUpload background migration' do
+ expect(Gitlab::BackgroundMigration).to receive(:steal).with('FillStoreUpload').and_call_original
+
+ migrate!
+ end
+
+ it 'does not run migration if not needed' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: 'uploads/-/system/avatar.jpg',
+ store: 1)
+
+ expect_any_instance_of(Gitlab::BackgroundMigration::FillStoreUpload).not_to receive(:perform)
+
+ migrate!
+ end
+
+ it 'ensures all rows are migrated' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: 'uploads/-/system/avatar.jpg',
+ store: nil)
+
+ expect_any_instance_of(Gitlab::BackgroundMigration::FillStoreUpload).to receive(:perform).and_call_original
+
+ expect do
+ migrate!
+ end.to change { uploads.where(store: nil).count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 95ae7bd21ab..96aa9a82b71 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,6 +25,9 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
+ it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
+ it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -107,6 +110,14 @@ describe ApplicationSetting do
it { expect(setting.repository_storages).to eq(['default']) }
end
+ context '#commit_email_hostname' do
+ it 'returns configured gitlab hostname if commit_email_hostname is not defined' do
+ setting.update(commit_email_hostname: nil)
+
+ expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}")
+ end
+ end
+
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2e65a6a2a0f..6849bc6db7a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1472,15 +1472,15 @@ describe Ci::Build do
end
describe '#retries_max' do
- context 'when max retries value is defined' do
- subject { create(:ci_build, options: { retry: 1 }) }
+ context 'with retries max config option' do
+ subject { create(:ci_build, options: { retry: { max: 1 } }) }
- it 'returns a number of configured max retries' do
+ it 'returns the number of configured max retries' do
expect(subject.retries_max).to eq 1
end
end
- context 'when max retries value is not defined' do
+ context 'without retries max config option' do
subject { create(:ci_build) }
it 'returns zero' do
@@ -1495,6 +1495,104 @@ describe Ci::Build do
expect(subject.retries_max).to eq 0
end
end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns the number of configured max retries' do
+ expect(subject.retries_max).to eq 1
+ end
+ end
+ end
+
+ describe '#retry_when' do
+ context 'with retries when config option' do
+ subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) }
+
+ it 'returns the configured when' do
+ expect(subject.retry_when).to eq ['some_reason']
+ end
+ end
+
+ context 'without retries when config option' do
+ subject { create(:ci_build) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+ end
+
+ describe '#retry_failure?' do
+ subject { create(:ci_build) }
+
+ context 'when retries max is zero' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(0)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max equals retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(1)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max is higher than retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(2)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ context 'and retry when is always' do
+ before do
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['always'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when includes the failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some_reason'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when does not include failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some', 'other failure'])
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+ end
end
end
@@ -2015,6 +2113,7 @@ describe Ci::Build do
{ key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
+ { key: 'CI_NODE_TOTAL', value: '1', public: true },
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
{ key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
@@ -2476,6 +2575,29 @@ describe Ci::Build do
end
end
+ context 'when build is parallelized' do
+ let(:total) { 5 }
+ let(:index) { 3 }
+
+ before do
+ build.options[:parallel] = total
+ build.options[:instance] = index
+ build.name = "#{build.name} #{index}/#{total}"
+ end
+
+ it 'includes CI_NODE_INDEX' do
+ is_expected.to include(
+ { key: 'CI_NODE_INDEX', value: index.to_s, public: true }
+ )
+ end
+
+ it 'includes correct CI_NODE_TOTAL' do
+ is_expected.to include(
+ { key: 'CI_NODE_TOTAL', value: total.to_s, public: true }
+ )
+ end
+ end
+
describe 'variables ordering' do
context 'when variables hierarchy is stubbed' do
let(:build_pre_var) { { key: 'build', value: 'value', public: true } }
@@ -2863,7 +2985,7 @@ describe Ci::Build do
end
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) }
+ subject { create(:ci_build, :running, options: { retry: { max: 3 } }, project: project, user: user) }
it 'retries build and assigns the same user to it' do
expect(described_class).to receive(:retry)
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 1580ef36127..6b0b23eeab3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Ingress do
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_knative
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
@@ -121,28 +122,5 @@ describe Clusters::Applications::Ingress do
expect(values).to include('stats')
expect(values).to include('podAnnotations')
end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 9c4396731eb..faaabafddb7 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe Clusters::Applications::Jupyter do
include_examples 'cluster application core specs', :clusters_applications_jupyter
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:oauth_application) }
@@ -79,29 +80,6 @@ describe Clusters::Applications::Jupyter do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
new file mode 100644
index 00000000000..be2a91d566b
--- /dev/null
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Knative do
+ let(:knative) { create(:clusters_applications_knative) }
+
+ include_examples 'cluster application core specs', :clusters_applications_knative
+ include_examples 'cluster application status specs', :clusters_applications_knative
+ include_examples 'cluster application helm specs', :clusters_applications_knative
+
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#make_installing!' do
+ before do
+ application.make_installing!
+ end
+
+ context 'application install previously errored with older version' do
+ let(:application) { create(:clusters_applications_knative, :scheduled, version: '0.1.3') }
+
+ it 'updates the application version' do
+ expect(application.reload.version).to eq('0.1.3')
+ end
+ end
+ end
+
+ describe '#make_installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#install_command' do
+ subject { knative.install_command }
+
+ it 'should be an instance of Helm::InstallCommand' do
+ expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
+ end
+
+ it 'should be initialized with knative arguments' do
+ expect(subject.name).to eq('knative')
+ expect(subject.chart).to eq('knative/knative')
+ expect(subject.version).to eq('0.1.3')
+ expect(subject.files).to eq(knative.files)
+ end
+ end
+
+ describe '#files' do
+ let(:application) { knative }
+ let(:values) { subject[:'values.yaml'] }
+
+ subject { application.files }
+
+ it 'should include knative specific keys in the values.yaml file' do
+ expect(values).to include('domain')
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:hostname) }
+ end
+end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 48ba163b38c..86de9dc60f2 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :clusters_applications_prometheus
+ include_examples 'cluster application helm specs', :clusters_applications_knative
describe '.installed' do
subject { described_class.installed }
@@ -187,29 +188,6 @@ describe Clusters::Applications::Prometheus do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include prometheus valid values' do
expect(values).to include('alertmanager')
expect(values).to include('kubeStateMetrics')
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index d5fb1a9d010..052cfdbc4b1 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do
include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status specs', :clusters_applications_runner
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:runner) }
@@ -74,29 +75,6 @@ describe Clusters::Applications::Runner do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 19b76ca8cfb..98d7e799d67 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -314,9 +314,10 @@ describe Clusters::Cluster do
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
end
end
end
@@ -342,4 +343,26 @@ describe Clusters::Cluster do
it { is_expected.to eq(false) }
end
end
+
+ describe '#allow_user_defined_namespace?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.allow_user_defined_namespace? }
+
+ context 'project type cluster' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'group type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'instance type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 2bcccc8184a..f5d261c4e9d 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -58,6 +58,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy }
end
+
+ context 'for group cluster' do
+ let(:namespace) { 'namespace-123' }
+ let(:cluster) { build(:cluster, :group, :provided_by_user) }
+ let(:kubernetes) { cluster.platform_kubernetes }
+
+ before do
+ kubernetes.namespace = namespace
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
context 'when validates api_url' do
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 8e88bb81162..0bc3ee014e6 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -92,4 +92,33 @@ describe Compare do
expect(subject.diff_refs.head_sha).to eq(head_commit.id)
end
end
+
+ describe '#modified_paths' do
+ context 'changes are present' do
+ let(:raw_compare) do
+ Gitlab::Git::Compare.new(
+ project.repository.raw_repository, 'before-create-delete-modify-move', 'after-create-delete-modify-move'
+ )
+ end
+
+ it 'returns affected file paths, without duplication' do
+ expect(subject.modified_paths).to contain_exactly(*%w{
+ foo/for_move.txt
+ foo/bar/for_move.txt
+ foo/for_create.txt
+ foo/for_delete.txt
+ foo/for_edit.txt
+ })
+ end
+ end
+
+ context 'changes are absent' do
+ let(:start_commit) { sample_commit }
+ let(:head_commit) { sample_commit }
+
+ it 'returns empty array' do
+ expect(subject.modified_paths).to eq([])
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 5713106418d..debc02fa51f 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -37,8 +37,8 @@ describe Awardable do
create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user)
create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user)
- expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3)
- expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3)
+ expect(Issue.awarded(award_emoji.user)).to eq [issue, issue3]
+ expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3]
end
end
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 951690a217b..17224c09693 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -14,40 +14,45 @@ describe EachBatch do
5.times { create(:user, updated_at: 1.day.ago) }
end
- it 'yields an ActiveRecord::Relation when a block is given' do
- model.each_batch do |relation|
- expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ shared_examples 'each_batch handling' do |kwargs|
+ it 'yields an ActiveRecord::Relation when a block is given' do
+ model.each_batch(kwargs) do |relation|
+ expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ end
end
- end
- it 'yields a batch index as the second argument' do
- model.each_batch do |_, index|
- expect(index).to eq(1)
+ it 'yields a batch index as the second argument' do
+ model.each_batch(kwargs) do |_, index|
+ expect(index).to eq(1)
+ end
end
- end
- it 'accepts a custom batch size' do
- amount = 0
+ it 'accepts a custom batch size' do
+ amount = 0
- model.each_batch(of: 1) { amount += 1 }
+ model.each_batch(kwargs.merge({ of: 1 })) { amount += 1 }
- expect(amount).to eq(5)
- end
+ expect(amount).to eq(5)
+ end
- it 'does not include ORDER BYs in the yielded relations' do
- model.each_batch do |relation|
- expect(relation.to_sql).not_to include('ORDER BY')
+ it 'does not include ORDER BYs in the yielded relations' do
+ model.each_batch do |relation|
+ expect(relation.to_sql).not_to include('ORDER BY')
+ end
end
- end
- it 'allows updating of the yielded relations' do
- time = Time.now
+ it 'allows updating of the yielded relations' do
+ time = Time.now
- model.each_batch do |relation|
- relation.update_all(updated_at: time)
- end
+ model.each_batch do |relation|
+ relation.update_all(updated_at: time)
+ end
- expect(model.where(updated_at: time).count).to eq(5)
+ expect(model.where(updated_at: time).count).to eq(5)
+ end
end
+
+ it_behaves_like 'each_batch handling', {}
+ it_behaves_like 'each_batch handling', { order_hint: :updated_at }
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index e121369f6ac..9a3f1f1c5a1 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -53,6 +53,25 @@ describe Environment do
end
end
+ describe '.with_deployment' do
+ subject { described_class.with_deployment(sha) }
+
+ let(:environment) { create(:environment) }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ context 'when deployment has the specified sha' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+
+ it { is_expected.to eq([environment]) }
+ end
+
+ context 'when deployment does not have the specified sha' do
+ let!(:deployment) { create(:deployment, environment: environment, sha: 'abc') }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '#folder_name' do
context 'when it is inside a folder' do
subject(:environment) do
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 52b98552184..90f7e4a4590 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe EnvironmentStatus do
+ include ProjectForksHelper
+
let(:deployment) { create(:deployment, :succeed, :review_app) }
- let(:environment) { deployment.environment}
+ let(:environment) { deployment.environment }
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
let(:sha) { deployment.sha }
@@ -65,9 +67,9 @@ describe EnvironmentStatus do
let(:admin) { create(:admin) }
let(:pipeline) { create(:ci_pipeline, sha: sha) }
- it 'is based on merge_request.head_pipeline' do
- expect(merge_request).to receive(:head_pipeline).and_return(pipeline)
- expect(merge_request).not_to receive(:merge_pipeline)
+ it 'is based on merge_request.diff_head_sha' do
+ expect(merge_request).to receive(:diff_head_sha)
+ expect(merge_request).not_to receive(:merge_commit_sha)
described_class.for_merge_request(merge_request, admin)
end
@@ -81,11 +83,83 @@ describe EnvironmentStatus do
merge_request.mark_as_merged!
end
- it 'is based on merge_request.merge_pipeline' do
- expect(merge_request).to receive(:merge_pipeline).and_return(pipeline)
- expect(merge_request).not_to receive(:head_pipeline)
+ it 'is based on merge_request.merge_commit_sha' do
+ expect(merge_request).to receive(:merge_commit_sha)
+ expect(merge_request).not_to receive(:diff_head_sha)
described_class.after_merge_request(merge_request, admin)
end
end
+
+ describe '.build_environments_status' do
+ subject { described_class.send(:build_environments_status, merge_request, user, sha) }
+
+ 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) }
+ let(:sha) { forked.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: forked,
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline)
+ end
+
+ it 'returns environment status' 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 created on a target project' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { project.commit.sha }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ head_pipeline: pipeline)
+ end
+
+ it 'returns environment status' 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
+
+ context 'when the build stops an environment' do
+ let!(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline) }
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
+
+ context 'when user does not have a permission to see the environment' do
+ let(:user) { create(:user) }
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 47e8f04e728..cbe60b3a4a5 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -232,4 +232,17 @@ describe MergeRequestDiff do
expect(commits.map(&:sha)).to match_array(commit_shas)
end
end
+
+ describe '#modified_paths' do
+ subject do
+ diff = create(:merge_request_diff)
+ create(:merge_request_diff_file, :new_file, merge_request_diff: diff)
+ create(:merge_request_diff_file, :renamed_file, merge_request_diff: diff)
+ diff
+ end
+
+ it 'returns affected file paths' do
+ expect(subject.modified_paths).to eq(%w{foo bar baz})
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3a54725c7ec..c7202b481d3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -631,6 +631,44 @@ describe MergeRequest do
end
end
+ describe '#modified_paths' do
+ let(:paths) { double(:paths) }
+ subject(:merge_request) { build(:merge_request) }
+
+ before do
+ expect(diff).to receive(:modified_paths).and_return(paths)
+ end
+
+ context 'when past_merge_request_diff is specified' do
+ let(:another_diff) { double(:merge_request_diff) }
+ let(:diff) { another_diff }
+
+ it 'returns affected file paths from specified past_merge_request_diff' do
+ expect(merge_request.modified_paths(past_merge_request_diff: another_diff)).to eq(paths)
+ end
+ end
+
+ context 'when compare is present' do
+ let(:compare) { double(:compare) }
+ let(:diff) { compare }
+
+ it 'returns affected file paths from compare' do
+ merge_request.compare = compare
+
+ expect(merge_request.modified_paths).to eq(paths)
+ end
+ end
+
+ context 'when no arguments provided' do
+ let(:diff) { merge_request.merge_request_diff }
+ subject(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+
+ it 'returns affected file paths for merge_request_diff' do
+ expect(merge_request.modified_paths).to eq(paths)
+ end
+ end
+ end
+
describe "#related_notes" do
let!(:merge_request) { create(:merge_request) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f020557e4af..471f19f9b7c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -4010,6 +4010,28 @@ describe Project do
end
end
+ describe '#snippets_visible?' do
+ it 'returns true when a logged in user can read snippets' do
+ project = create(:project, :public)
+ user = create(:user)
+
+ expect(project.snippets_visible?(user)).to eq(true)
+ end
+
+ it 'returns true when an anonymous user can read snippets' do
+ project = create(:project, :public)
+
+ expect(project.snippets_visible?).to eq(true)
+ end
+
+ it 'returns false when a user can not read snippets' do
+ project = create(:project, :private)
+ user = create(:user)
+
+ expect(project.snippets_visible?(user)).to eq(false)
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index e09d89d235d..7a7272ccb60 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -131,6 +131,217 @@ describe Snippet do
end
end
+ describe '.with_optional_visibility' do
+ context 'when a visibility level is provided' do
+ it 'returns snippets with the given visibility' do
+ create(:snippet, :private)
+
+ snippet = create(:snippet, :public)
+ snippets = described_class
+ .with_optional_visibility(Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when a visibility level is not provided' do
+ it 'returns all snippets' do
+ snippet1 = create(:snippet, :public)
+ snippet2 = create(:snippet, :private)
+ snippets = described_class.with_optional_visibility
+
+ expect(snippets).to include(snippet1, snippet2)
+ end
+ end
+ end
+
+ describe '.only_global_snippets' do
+ it 'returns snippets not associated with any projects' do
+ create(:project_snippet)
+
+ snippet = create(:snippet)
+ snippets = described_class.only_global_snippets
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.only_include_projects_visible_to' do
+ let!(:project1) { create(:project, :public) }
+ let!(:project2) { create(:project, :internal) }
+ let!(:project3) { create(:project, :private) }
+ let!(:snippet1) { create(:project_snippet, project: project1) }
+ let!(:snippet2) { create(:project_snippet, project: project2) }
+ let!(:snippet3) { create(:project_snippet, project: project3) }
+
+ context 'when a user is provided' do
+ it 'returns snippets visible to the user' do
+ user = create(:user)
+
+ snippets = described_class.only_include_projects_visible_to(user)
+
+ expect(snippets).to include(snippet1, snippet2)
+ expect(snippets).not_to include(snippet3)
+ end
+ end
+
+ context 'when a user is not provided' do
+ it 'returns snippets visible to anonymous users' do
+ snippets = described_class.only_include_projects_visible_to
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+ end
+ end
+
+ describe 'only_include_projects_with_snippets_enabled' do
+ context 'when the include_private option is enabled' do
+ it 'includes snippets for projects with snippets set to private' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PRIVATE)
+
+ snippet = create(:project_snippet, project: project)
+
+ snippets = described_class
+ .only_include_projects_with_snippets_enabled(include_private: true)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when the include_private option is not enabled' do
+ it 'does not include snippets for projects that have snippets set to private' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PRIVATE)
+
+ create(:project_snippet, project: project)
+
+ snippets = described_class.only_include_projects_with_snippets_enabled
+
+ expect(snippets).to be_empty
+ end
+ end
+
+ it 'includes snippets for projects with snippets enabled' do
+ project = create(:project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ snippet = create(:project_snippet, project: project)
+ snippets = described_class.only_include_projects_with_snippets_enabled
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.only_include_authorized_projects' do
+ it 'only includes snippets for projects the user is authorized to see' do
+ user = create(:user)
+ project1 = create(:project, :private)
+ project2 = create(:project, :private)
+
+ project1.team.add_developer(user)
+
+ create(:project_snippet, project: project2)
+
+ snippet = create(:project_snippet, project: project1)
+ snippets = described_class.only_include_authorized_projects(user)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ describe '.for_project_with_user' do
+ context 'when a user is provided' do
+ it 'returns an empty collection if the user can not view the snippets' do
+ project = create(:project, :private)
+ user = create(:user)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ create(:project_snippet, :public, project: project)
+
+ expect(described_class.for_project_with_user(project, user)).to be_empty
+ end
+
+ it 'returns the snippets if the user is a member of the project' do
+ project = create(:project, :private)
+ user = create(:user)
+ snippet = create(:project_snippet, project: project)
+
+ project.team.add_developer(user)
+
+ snippets = described_class.for_project_with_user(project, user)
+
+ expect(snippets).to eq([snippet])
+ end
+
+ it 'returns public snippets for a public project the user is not a member of' do
+ project = create(:project, :public)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ user = create(:user)
+ snippet = create(:project_snippet, :public, project: project)
+
+ create(:project_snippet, :private, project: project)
+
+ snippets = described_class.for_project_with_user(project, user)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+
+ context 'when a user is not provided' do
+ it 'returns an empty collection for a private project' do
+ project = create(:project, :private)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::ENABLED)
+
+ create(:project_snippet, :public, project: project)
+
+ expect(described_class.for_project_with_user(project)).to be_empty
+ end
+
+ it 'returns public snippets for a public project' do
+ project = create(:project, :public)
+ snippet = create(:project_snippet, :public, project: project)
+
+ project.project_feature
+ .update(snippets_access_level: ProjectFeature::PUBLIC)
+
+ create(:project_snippet, :private, project: project)
+
+ snippets = described_class.for_project_with_user(project)
+
+ expect(snippets).to eq([snippet])
+ end
+ end
+ end
+
+ describe '.visible_to_or_authored_by' do
+ it 'returns snippets visible to the user' do
+ user = create(:user)
+ snippet1 = create(:snippet, :public)
+ snippet2 = create(:snippet, :private, author: user)
+ snippet3 = create(:snippet, :private)
+
+ snippets = described_class.visible_to_or_authored_by(user)
+
+ expect(snippets).to include(snippet1, snippet2)
+ expect(snippets).not_to include(snippet3)
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 3c89e99abf0..5a0df9fbbb0 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -21,7 +21,8 @@ describe Upload do
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
model: build_stubbed(:user),
- uploader: double('ExampleUploader')
+ uploader: double('ExampleUploader'),
+ store: ObjectStorage::Store::LOCAL
)
expect(UploadChecksumWorker)
@@ -35,7 +36,8 @@ describe Upload do
path: __FILE__,
size: described_class::CHECKSUM_THRESHOLD,
model: build_stubbed(:user),
- uploader: double('ExampleUploader')
+ uploader: double('ExampleUploader'),
+ store: ObjectStorage::Store::LOCAL
)
expect { upload.save }
@@ -60,7 +62,7 @@ describe Upload do
describe '#absolute_path' do
it 'returns the path directly when already absolute' do
path = '/path/to/namespace/project/secret/file.jpg'
- upload = described_class.new(path: path)
+ upload = described_class.new(path: path, store: ObjectStorage::Store::LOCAL)
expect(upload).not_to receive(:uploader_class)
@@ -69,7 +71,7 @@ describe Upload do
it "delegates to the uploader's absolute_path method" do
uploader = spy('FakeUploader')
- upload = described_class.new(path: 'secret/file.jpg')
+ upload = described_class.new(path: 'secret/file.jpg', store: ObjectStorage::Store::LOCAL)
expect(upload).to receive(:uploader_class).and_return(uploader)
upload.absolute_path
@@ -81,7 +83,8 @@ describe Upload do
describe '#calculate_checksum!' do
let(:upload) do
described_class.new(path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte)
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte,
+ store: ObjectStorage::Store::LOCAL)
end
it 'sets `checksum` to SHA256 sum of the file' do
@@ -104,15 +107,56 @@ describe Upload do
describe '#exist?' do
it 'returns true when the file exists' do
- upload = described_class.new(path: __FILE__)
+ upload = described_class.new(path: __FILE__, store: ObjectStorage::Store::LOCAL)
expect(upload).to exist
end
- it 'returns false when the file does not exist' do
- upload = described_class.new(path: "#{__FILE__}-nope")
+ context 'when the file does not exist' do
+ it 'returns false' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
- expect(upload).not_to exist
+ expect(upload).not_to exist
+ end
+
+ context 'when the record is persisted' do
+ it 'sends a message to Sentry' do
+ upload = create(:upload, :issuable_upload)
+
+ expect(Gitlab::Sentry).to receive(:enabled?).and_return(true)
+ expect(Raven).to receive(:capture_message).with("Upload file does not exist", extra: upload.attributes)
+
+ upload.exist?
+ end
+
+ it 'increments a metric counter to signal a problem' do
+ upload = create(:upload, :issuable_upload)
+
+ counter = double(:counter)
+ expect(counter).to receive(:increment)
+ expect(Gitlab::Metrics).to receive(:counter).with(:upload_file_does_not_exist_total, 'The number of times an upload record could not find its file').and_return(counter)
+
+ upload.exist?
+ end
+ end
+
+ context 'when the record is not persisted' do
+ it 'does not send a message to Sentry' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
+
+ expect(Raven).not_to receive(:capture_message)
+
+ upload.exist?
+ end
+
+ it 'does not increment a metric counter' do
+ upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL)
+
+ expect(Gitlab::Metrics).not_to receive(:counter)
+
+ upload.exist?
+ end
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4e7c8523e65..0ac5bd666ae 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -183,6 +183,12 @@ describe User do
expect(found_user.commit_email).to eq(user.email)
end
+ it 'returns the private commit email when commit_email has _private' do
+ user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user.commit_email).to eq(user.private_commit_email)
+ end
+
it 'can be set to a confirmed email' do
confirmed = create(:email, :confirmed, user: user)
user.commit_email = confirmed.email
@@ -333,6 +339,40 @@ describe User do
expect(user).to be_valid
end
end
+
+ context 'set_commit_email' do
+ it 'keeps commit email when private commit email is being used' do
+ user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
+ end
+
+ it 'keeps the commit email when nil' do
+ user = create(:user, commit_email: nil)
+
+ expect(user.read_attribute(:commit_email)).to be_nil
+ end
+
+ it 'reverts to nil when email is not verified' do
+ user = create(:user, commit_email: "foo@bar.com")
+
+ expect(user.read_attribute(:commit_email)).to be_nil
+ end
+ end
+
+ context 'owns_commit_email' do
+ it 'accepts private commit email' do
+ user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
+
+ expect(user).to be_valid
+ end
+
+ it 'accepts nil commit email' do
+ user = build(:user, commit_email: nil)
+
+ expect(user).to be_valid
+ end
+ end
end
end
@@ -1075,6 +1115,14 @@ describe User do
end
describe '.find_by_any_email' do
+ it 'finds user through private commit email' do
+ user = create(:user)
+ private_email = user.private_commit_email
+
+ expect(described_class.find_by_any_email(private_email)).to eq(user)
+ expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user)
+ end
+
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
@@ -1082,6 +1130,13 @@ describe User do
expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user
end
+ it 'finds by uppercased email' do
+ user = create(:user, email: 'foo@example.com')
+
+ expect(described_class.find_by_any_email(user.email.upcase)).to eq user
+ expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user
+ end
+
it 'finds by secondary email' do
email = create(:email, email: 'foo@example.com')
user = email.user
@@ -1457,7 +1512,7 @@ describe User do
email_confirmed = create :email, user: user, confirmed_at: Time.now
create :email, user: user
- expect(user.verified_emails).to match_array([user.email, email_confirmed.email])
+ expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email])
end
end
@@ -1473,6 +1528,10 @@ describe User do
expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy
end
+ it 'returns true when user is found through private commit email' do
+ expect(user.verified_email?(user.private_commit_email)).to be_truthy
+ end
+
it 'returns false when the email is not verified/confirmed' do
email_unconfirmed = create :email, user: user
user.reload
@@ -1668,6 +1727,24 @@ describe User do
end
end
+ describe '.find_by_private_commit_email' do
+ context 'with email' do
+ set(:user) { create(:user) }
+
+ it 'returns user through private commit email' do
+ expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user)
+ end
+
+ it 'returns nil when email other than private_commit_email is used' do
+ expect(described_class.find_by_private_commit_email(user.email)).to be_nil
+ end
+ end
+
+ it 'returns nil when email is nil' do
+ expect(described_class.find_by_private_commit_email(nil)).to be_nil
+ end
+ end
+
describe '#sort_by_attribute' do
before do
described_class.delete_all
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 821946a47e5..b87a2d871e5 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -126,23 +126,34 @@ describe WikiPage do
end
end
- before do
- @wiki_attr = { title: "Index", content: "Home Page", format: "markdown" }
- end
-
describe "#create" do
+ let(:wiki_attr) do
+ {
+ title: "Index",
+ content: "Home Page",
+ format: "markdown",
+ message: 'Custom Commit Message'
+ }
+ end
+
after do
destroy_page("Index")
end
context "with valid attributes" do
it "saves the wiki page" do
- subject.create(@wiki_attr)
+ subject.create(wiki_attr)
expect(wiki.find_page("Index")).not_to be_nil
end
it "returns true" do
- expect(subject.create(@wiki_attr)).to eq(true)
+ expect(subject.create(wiki_attr)).to eq(true)
+ end
+
+ it 'saves the wiki page with message' do
+ subject.create(wiki_attr)
+
+ expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
end
end
end
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index ced969830d8..b2f0ca1bc30 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -24,5 +24,47 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.group }
+ let(:project) { create(:project, namespace: group) }
+
+ context 'when group developer' do
+ before do
+ group.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when group maintainer' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'when project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when project developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+ end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5e583be457e..9d0093e8159 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -21,7 +21,11 @@ describe GroupPolicy do
let(:maintainer_permissions) do
[
- :create_projects
+ :create_projects,
+ :read_cluster,
+ :create_cluster,
+ :update_cluster,
+ :admin_cluster
]
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index b7ec35d6ec5..d6bc67a9d70 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -163,7 +163,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
@@ -182,7 +182,7 @@ describe ProjectPolicy do
:create_build, :read_build, :update_build, :admin_build, :destroy_build,
:create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
]
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 7af181f37d5..72c5eac3ede 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -82,5 +82,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(project_cluster_path(project, cluster)) }
end
+
+ context 'group_type cluster' do
+ let(:group) { cluster.group }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
end
end
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..205160742bf
--- /dev/null
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(group) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+ let(:group) { cluster.group }
+
+ describe '#can_create_cluster?' do
+ let(:user) { create(:user) }
+
+ subject { presenter.can_create_cluster? }
+
+ before do
+ allow(presenter).to receive(:current_user).and_return(user)
+ end
+
+ context 'when user can create' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when user cannot create' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#index_path' do
+ subject { presenter.index_path }
+
+ it { is_expected.to eq(group_clusters_path(group)) }
+ end
+
+ describe '#new_path' do
+ subject { presenter.new_path }
+
+ it { is_expected.to eq(new_group_cluster_path(group)) }
+ end
+
+ describe '#create_user_clusters_path' do
+ subject { presenter.create_user_clusters_path }
+
+ it { is_expected.to eq(create_user_group_clusters_path(group)) }
+ end
+
+ describe '#create_gcp_clusters_path' do
+ subject { presenter.create_gcp_clusters_path }
+
+ it { is_expected.to eq(create_gcp_group_clusters_path(group)) }
+ end
+
+ describe '#cluster_status_cluster_path' do
+ subject { presenter.cluster_status_cluster_path(cluster) }
+
+ it { is_expected.to eq(cluster_status_group_cluster_path(group, cluster)) }
+ end
+
+ describe '#install_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.install_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
+ end
+
+ describe '#cluster_path' do
+ subject { presenter.cluster_path(cluster) }
+
+ it { is_expected.to eq(group_cluster_path(group, cluster)) }
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 2963dea634a..329d069ef3d 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1208,6 +1208,118 @@ describe API::Commits do
end
end
+ describe 'POST :id/repository/commits/:sha/revert' do
+ let(:commit_id) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let(:commit) { project.commit(commit_id) }
+ let(:branch) { 'master' }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/revert" }
+
+ shared_examples_for 'ref revert' do
+ context 'when ref exists' do
+ it 'reverts the ref commit' do
+ post api(route, current_user), branch: branch
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit/basic')
+
+ expect(json_response['message']).to eq(commit.revert_message(user))
+ expect(json_response['author_name']).to eq(user.name)
+ expect(json_response['committer_name']).to eq(user.name)
+ expect(json_response['parent_ids']).to contain_exactly(commit_id)
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route), branch: branch }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route), branch: branch }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as an owner' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref revert'
+
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when branch is missing' do
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+
+ context 'when branch is empty' do
+ ['', ' '].each do |branch|
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user), branch: branch }
+ end
+ end
+ end
+
+ context 'when branch does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'foo' }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when ref contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+ let(:commit) { project.repository.commit(commit_id) }
+
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when branch is protected' do
+ before do
+ create(:protected_branch, project: project, name: 'feature')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ post api(route, current_user), branch: 'feature'
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to match(/You are not allowed to push into this branch/)
+ end
+ end
+ end
+ end
+
describe 'POST /projects/:id/repository/commits/:sha/comments' do
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5ea869796b0..2ebcb787d06 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -683,7 +683,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
@@ -704,7 +704,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
@@ -837,7 +837,7 @@ describe API::Internal do
expect(json_response['merge_request_urls']).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb
new file mode 100644
index 00000000000..fa447c028c2
--- /dev/null
+++ b/spec/requests/api/submodules_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Submodules do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let(:guest) { create(:user) { |u| project.add_guest(u) } }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:branch) { 'master' }
+ let(:commit_message) { 'whatever' }
+
+ let(:params) do
+ {
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ commit_message: commit_message
+ }
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ def route(submodule = nil)
+ "/projects/#{project.id}/repository/submodules/#{submodule}"
+ end
+
+ describe "PUT /projects/:id/repository/submodule/:submodule" do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ put api(route(submodule)), params
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it 'returns 403' do
+ put api(route(submodule), guest), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it 'returns 400 if params is missing' do
+ put api(route(submodule), user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if branch is missing' do
+ put api(route(submodule), user), params.except(:branch)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if commit_sha is missing' do
+ put api(route(submodule), user), params.except(:commit_sha)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns the commmit' do
+ head_commit = project.repository.commit.id
+
+ put api(route(submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['message']).to eq commit_message
+ expect(json_response['author_name']).to eq user.name
+ expect(json_response['committer_name']).to eq user.name
+ expect(json_response['parent_ids'].first).to eq head_commit
+ end
+
+ context 'when the submodule name is urlencoded' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch) { 'submodule_inside_folder' }
+ let(:encoded_submodule) { CGI.escape(submodule) }
+
+ it 'returns the commmit' do
+ expect(Submodules::UpdateService)
+ .to receive(:new)
+ .with(any_args, hash_including(submodule: submodule))
+ .and_call_original
+
+ put api(route(encoded_submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq project.repository.commit(branch).id
+ expect(project.repository.blob_at(branch, submodule).id).to eq commit_sha
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index 962ec919092..52bd40ecb5e 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -15,6 +15,7 @@ describe EnvironmentStatusEntity do
subject { entity.as_json }
before do
+ deployment.update(sha: merge_request.diff_head_sha)
allow(request).to receive(:current_user).and_return(user)
end
diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
deleted file mode 100644
index 06d9d3657e6..00000000000
--- a/spec/serializers/issue_board_entity_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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 e8c46c0cdee..75578816e75 100644
--- a/spec/serializers/issue_serializer_spec.rb
+++ b/spec/serializers/issue_serializer_spec.rb
@@ -24,12 +24,4 @@ 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/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 054b7b1561c..5c87ed5c3c6 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -435,16 +435,34 @@ describe Ci::CreatePipelineService do
end
context 'when builds with auto-retries are configured' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
- stub_ci_pipeline_yaml_file(config)
+ context 'as an integer' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
+ end
end
- it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
+ context 'as hash' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
+ stub_ci_pipeline_yaml_file(config)
+ end
- expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
+ end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 8c7258c42ad..538992b621e 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -671,9 +671,9 @@ describe Ci::ProcessPipelineService, '#execute' do
context 'when builds with auto-retries are configured' do
before do
- create_build('build:1', stage_idx: 0, user: user, options: { retry: 2 })
+ create_build('build:1', stage_idx: 0, user: user, options: { retry: { max: 2 } })
create_build('test:1', stage_idx: 1, user: user, when: :on_failure)
- create_build('test:2', stage_idx: 1, user: user, options: { retry: 1 })
+ create_build('test:2', stage_idx: 1, user: user, options: { retry: { max: 1 } })
end
it 'automatically retries builds in a valid order' do
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 056db0c5486..0bd7719345e 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -60,6 +60,31 @@ describe Clusters::Applications::CreateService do
end
end
+ context 'knative application' do
+ let(:params) do
+ {
+ application: 'knative',
+ hostname: 'example.com'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_knative)
+ end
+
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
+ end
+
context 'invalid application' do
let(:params) { { application: 'non-existent' } }
@@ -67,5 +92,39 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ where(:application, :association, :allowed) do
+ 'helm' | :application_helm | true
+ 'ingress' | :application_ingress | true
+ 'runner' | :application_runner | false
+ 'jupyter' | :application_jupyter | false
+ 'prometheus' | :application_prometheus | false
+ end
+
+ with_them do
+ let(:params) { { application: application } }
+
+ it 'executes for each application' do
+ if allowed
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, association)
+ else
+ expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 7fbb6cf2cf5..efee158739d 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -33,7 +33,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
- expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip)
expect(platform.username).to eq(username)
expect(platform.password).to eq(password)
expect(platform.token).to eq(token)
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index a1b20c61116..73f9be242a3 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -62,5 +62,32 @@ describe Clusters::UpdateService do
expect(cluster.errors[:"platform_kubernetes.namespace"]).to be_present
end
end
+
+ context 'when cluster is provided by GCP' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ let(:params) do
+ {
+ name: 'my-new-name'
+ }
+ end
+
+ it 'does not change cluster name' do
+ is_expected.to eq(false)
+
+ cluster.reload
+ expect(cluster.name).to eq('test-cluster')
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp) }
+
+ it 'rejects changes' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors.full_messages).to include('cannot modify during creation')
+ end
+ end
+ end
end
end
diff --git a/spec/services/commits/commit_patch_service_spec.rb b/spec/services/commits/commit_patch_service_spec.rb
new file mode 100644
index 00000000000..f4fcec2fbc2
--- /dev/null
+++ b/spec/services/commits/commit_patch_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Commits::CommitPatchService do
+ describe '#execute' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ [content_1, content_2]
+ end
+ let(:user) { project.creator }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:project) { create(:project, :repository) }
+ let(:start_branch) { nil }
+ let(:params) { { branch_name: branch_name, patches: patches, start_branch: start_branch } }
+
+ subject(:service) do
+ described_class.new(project, user, params)
+ end
+
+ it 'returns a successful result' do
+ result = service.execute
+
+ branch = project.repository.find_branch(branch_name)
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:result]).to eq(branch.target)
+ end
+
+ it 'is based off HEAD when no start ref is passed' do
+ service.execute
+
+ merge_base = project.repository.merge_base(project.repository.root_ref, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit('HEAD').sha)
+ end
+
+ context 'when specifying a different start branch' do
+ let(:start_branch) { 'with-codeowners' }
+
+ it 'is based of the correct branch' do
+ service.execute
+
+ merge_base = project.repository.merge_base(start_branch, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit(start_branch).sha)
+ end
+ end
+
+ shared_examples 'an error response' do |expected_message|
+ it 'returns the correct error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to match(expected_message)
+ end
+ end
+
+ context 'when the user does not have access' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'an error response',
+ 'You are not allowed to push into this branch'
+ end
+
+ context 'when the patches are not valid' do
+ let(:patches) { "a" * 2.1.megabytes }
+
+ it_behaves_like 'an error response', 'Patches are too big'
+ end
+
+ context 'when the new branch name is invalid' do
+ let(:branch_name) { 'HEAD' }
+
+ it_behaves_like 'an error response', 'Branch name is invalid'
+ end
+
+ context 'when the patches do not apply' do
+ let(:branch_name) { 'feature' }
+
+ it_behaves_like 'an error response', 'Patch failed at'
+ end
+
+ context 'when specifying a non existent start branch' do
+ let(:start_branch) { 'does-not-exist' }
+
+ it_behaves_like 'an error response', 'Invalid reference name'
+ end
+ end
+end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 53c85f73cde..f0b0f7956ce 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Issuable::BulkUpdateService do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
def bulk_update(issuables, extra_params = {})
bulk_update_params = extra_params
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 9f1da7d9419..c9a668994eb 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -392,5 +392,13 @@ describe MergeRequests::BuildService do
expect(merge_request.source_project).to eq(project)
end
end
+
+ context 'when specifying target branch in the description' do
+ let(:description) { "A merge request targeting another branch\n\n/target_branch with-codeowners" }
+
+ it 'sets the attribute from the quick actions' do
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 3e33a165e55..274624aa8bb 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
let(:source_branch) { "merge-test" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{source_branch}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
@@ -117,7 +117,7 @@ describe MergeRequests::GetUrlsService do
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
it 'returns 2 urls for both creating new and showing merge request' do
result = service.execute(changes)
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 1b599ba11b6..be5ad849ba7 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -593,8 +593,8 @@ describe MergeRequests::UpdateService, :mailer do
end
context 'setting `allow_collaboration`' do
- let(:target_project) { create(:project, :public) }
- let(:source_project) { fork_project(target_project) }
+ let(:target_project) { create(:project, :repository, :public) }
+ let(:source_project) { fork_project(target_project, nil, repository: true) }
let(:user) { create(:user) }
let(:merge_request) do
create(:merge_request,
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 8680e428517..9d2be30c636 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Milestones::DestroyService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
before do
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index a8c994c101c..14d62763a5b 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Notes::QuickActionsService do
shared_context 'note on noteable' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) }
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index e513ee7ae44..5a7cafcb60f 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1213,6 +1213,15 @@ describe QuickActions::InterpretService do
end
end
end
+
+ it 'limits to commands passed ' do
+ content = "/shrug\n/close"
+
+ text, commands = service.execute(content, issue, only: [:shrug])
+
+ expect(commands).to be_empty
+ expect(text).to eq("#{described_class::SHRUG}\n/close")
+ end
end
describe '#explain' do
diff --git a/spec/services/submodules/update_service_spec.rb b/spec/services/submodules/update_service_spec.rb
new file mode 100644
index 00000000000..cf92350c1b2
--- /dev/null
+++ b/spec/services/submodules/update_service_spec.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Submodules::UpdateService do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user, :commit_email) }
+ let(:branch_name) { project.default_branch }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:commit_message) { 'whatever' }
+ let(:current_sha) { repository.blob_at('HEAD', submodule).id }
+ let(:commit_params) do
+ {
+ submodule: submodule,
+ commit_message: commit_message,
+ commit_sha: commit_sha,
+ branch_name: branch_name
+ }
+ end
+
+ subject { described_class.new(project, user, commit_params) }
+
+ describe "#execute" do
+ shared_examples 'returns error result' do
+ it do
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
+ end
+ end
+
+ context 'when the user is not authorized' do
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'when the user is authorized' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, :no_one_can_push, project: project, name: branch_name)
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'validations' do
+ context 'when submodule' do
+ context 'is empty' do
+ let(:submodule) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:submodule) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:submodule) { 'VERSION' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:submodule) { 'non-existent-submodule' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'has traversal path' do
+ let(:submodule) { '../six' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+ end
+
+ context 'commit_sha' do
+ context 'is empty' do
+ let(:commit_sha) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:commit_sha) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:commit_sha) { '1' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is the same as the current ref' do
+ let(:commit_sha) { current_sha }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { "The submodule #{submodule} is already at #{commit_sha}" }
+ end
+ end
+ end
+
+ context 'branch_name' do
+ context 'is empty' do
+ let(:branch_name) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'is not present' do
+ let(:branch_name) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:branch_name) { 'non/existent-branch' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'when commit message is empty' do
+ let(:commit_message) { '' }
+
+ it 'a default commit message is set' do
+ message = "Update submodule #{submodule} with oid #{commit_sha}"
+
+ expect(repository).to receive(:update_submodule).with(any_args, hash_including(message: message))
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when there is an unexpected error' do
+ before do
+ allow(repository).to receive(:update_submodule).and_raise(StandardError, 'error message')
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'error message' }
+ end
+ end
+
+ it 'updates the submodule reference' do
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ expect(result[:result]).to eq repository.head_commit.id
+ expect(repository.blob_at('HEAD', submodule).id).to eq commit_sha
+ end
+
+ context 'when submodule is inside a directory' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch_name) { 'submodule_inside_folder' }
+
+ it 'updates the submodule reference' do
+ expect(repository.blob_at(branch_name, submodule).id).not_to eq commit_sha
+
+ subject.execute
+
+ expect(repository.blob_at(branch_name, submodule).id).to eq commit_sha
+ end
+ end
+
+ context 'when repository is empty' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:branch_name) { 'master' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'The repository is empty' }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 1746721b0d0..c52515aefd8 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -10,7 +10,7 @@ describe TodoService do
let(:john_doe) { create(:user) }
let(:skipped) { create(:user) }
let(:skip_users) { [skipped] }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') }
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 80604395adf..18cf08f0b9e 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -150,17 +150,25 @@ shared_examples 'discussion comments' do |resource_name|
end
if resource_name == 'merge request'
- let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] }
+ 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
+
click_button "Resolve discussion"
+ wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
refresh
- click_button "Toggle discussion"
+ click_button "1 reply"
- expect(page).to have_selector(".note-row-#{note_id}", visible: true)
+ expect(page).to have_selector(".note-row-#{reply_id}", visible: true)
end
end
end
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 89a5518239d..8cfce49da8a 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
+ expect(dropdown).to have_link('Report abuse to GitLab', href: abuse_report_path)
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report as abuse')
+ dropdown.click_link('Report abuse to GitLab')
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 0c35764ed9a..5887c3eab74 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -1,6 +1,10 @@
module MigrationsHelpers
+ def active_record_base
+ ActiveRecord::Base
+ end
+
def table(name)
- Class.new(ActiveRecord::Base) do
+ Class.new(active_record_base) do
self.table_name = name
self.inheritance_column = :_type_disabled
@@ -19,7 +23,7 @@ module MigrationsHelpers
end
def clear_schema_cache!
- ActiveRecord::Base.connection_pool.connections.each do |conn|
+ active_record_base.connection_pool.connections.each do |conn|
conn.schema_cache.clear!
end
end
@@ -40,11 +44,18 @@ module MigrationsHelpers
# Reset column information for the most offending classes **after** we
# migrated the schema up, otherwise, column information could be
# outdated. We have a separate method for this so we can override it in EE.
- ActiveRecord::Base.descendants.each(&method(:reset_column_information))
+ active_record_base.descendants.each(&method(:reset_column_information))
+ end
- # Without that, we get errors because of missing attributes, e.g.
+ def refresh_attribute_methods
+ # Without this, we get errors because of missing attributes, e.g.
# super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
- ApplicationSetting.define_attribute_methods
+ # attr_encrypted also expects ActiveRecord attribute methods to be
+ # defined, or it will override the accessors:
+ # https://gitlab.com/gitlab-org/gitlab-ee/issues/8234#note_113976421
+ [ApplicationSetting, SystemHook].each do |model|
+ model.define_attribute_methods
+ end
end
def reset_column_information(klass)
@@ -84,6 +95,7 @@ module MigrationsHelpers
end
reset_column_in_all_models
+ refresh_attribute_methods
end
def disable_migrations_output
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 71d72ff27e9..1f00cdf7e92 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -55,7 +55,11 @@ module TestEnv
'update-gitlab-shell-v-6-0-1' => '2f61d70',
'update-gitlab-shell-v-6-0-3' => 'de78448',
'2-mb-file' => 'bf12d25',
- 'with-codeowners' => '219560e'
+ 'before-create-delete-modify-move' => '845009f',
+ 'between-create-delete-modify-move' => '3f5f443',
+ 'after-create-delete-modify-move' => 'ba3faa7',
+ 'with-codeowners' => '219560e',
+ 'submodule_inside_folder' => 'b491b92'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -158,8 +162,9 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do
- start_gitaly(gitaly_dir)
- end
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ start_gitaly(gitaly_dir)
+ end
end
def start_gitaly(gitaly_dir)
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 9373de5aeab..7038a366144 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -17,10 +17,10 @@ RSpec.shared_examples 'a creatable merge request' do
sign_in(user)
visit project_new_merge_request_path(
target_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index 361d4220c6e..ba9b7d3bdcf 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -2,12 +2,12 @@ shared_examples 'helm commands' do
describe '#generate_script' do
let(:helm_setup) do
<<~EOS
- set -eo pipefail
+ set -xeo pipefail
EOS
end
it 'should return appropriate command' do
- expect(subject.generate_script).to eq(helm_setup + commands)
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
end
end
end
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
new file mode 100644
index 00000000000..d87b3181e80
--- /dev/null
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -0,0 +1,25 @@
+shared_examples 'cluster application helm specs' do |application_name|
+ let(:application) { create(application_name) }
+
+ describe '#files' do
+ subject { application.files }
+
+ context 'when the helm application does not have a ca_cert' do
+ before do
+ application.cluster.application_helm.ca_cert = nil
+ end
+
+ it 'should not include cert files when there is no ca_cert entry' do
+ expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
+ end
+ end
+
+ it 'should include cert files when there is a ca_cert entry' do
+ expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
+ expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
+
+ cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
+ expect(cert.not_after).to be < 60.minutes.from_now
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
index 1aed8ab0113..668a390b5d2 100644
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -16,7 +16,12 @@ shared_examples 'merge requests list' do
create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
- create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request.metrics.update!(merged_by: user,
+ latest_closed_by: user,
+ latest_closed_at: 1.hour.ago,
+ merged_at: 2.hours.ago)
expect do
get api(endpoint_path, user)
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index 6fcfae358ec..9588e8be5dc 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -38,14 +38,6 @@ describe 'gitlab:uploads:migrate rake tasks' do
let!(:projects) { create_list(:project, 10, :with_avatar) }
it_behaves_like 'enqueue jobs in batch', batch: 4
-
- context 'Upload has store = nil' do
- before do
- Upload.where(model: projects).update_all(store: nil)
- end
-
- it_behaves_like 'enqueue jobs in batch', batch: 4
- end
end
context "for Group" do
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index 9c0be249a50..8a9ab02eaca 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -12,10 +12,10 @@ describe 'projects/notes/_more_actions_dropdown' do
assign(:project, project)
end
- it 'shows Report as abuse button if not editable and not current users comment' do
+ it 'shows Report abuse to GitLab button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
- expect(rendered).to have_link('Report as abuse')
+ expect(rendered).to have_link('Report abuse to GitLab')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -24,10 +24,10 @@ describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report as abuse and Delete buttons if editable and not current users comment' do
+ it 'shows Report abuse to GitLab and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
- expect(rendered).to have_link('Report as abuse')
+ expect(rendered).to have_link('Report abuse to GitLab')
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index e4e77c667b3..045135255d6 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -46,6 +46,21 @@ describe EmailReceiverWorker, :mailer do
should_not_email_anyone
end
end
+
+ context 'when the error is Gitlab::Email::InvalidAttachment' do
+ let(:error) { Gitlab::Email::InvalidAttachment.new("Could not deal with that") }
+
+ it 'reports the error to the sender' do
+ perform_enqueued_jobs do
+ described_class.new.perform(raw_message)
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.body.parts.last.to_s).to include("Could not deal with that")
+ end
+ end
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index 2169c14218b..e94d2be9850 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do
context 'when the import status was already updated' do
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
- project.import_start
- project.import_finish
+ import_state.start
+ import_state.finish
- [project.import_jid]
+ [import_state.jid]
end
end
it 'does not mark the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('finished')
+ expect(import_state.reload.status).to eq('finished')
end
end
context 'when the import status was not updated' do
before do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([project.import_jid])
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([import_state.jid])
end
it 'marks the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('failed')
+ expect(import_state.reload.status).to eq('failed')
end
end
end
@@ -41,27 +41,27 @@ describe StuckImportJobsWorker do
end
it 'does not mark the project as failed' do
- expect { worker.perform }.not_to change { project.reload.import_status }
+ expect { worker.perform }.not_to change { import_state.reload.status }
end
end
end
describe 'with scheduled import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_scheduled) }
+ let(:import_state) { create(:project, :import_scheduled).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
describe 'with started import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_started) }
+ let(:import_state) { create(:project, :import_started).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 39b6783cef8..69eda01429a 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -1,6 +1,7 @@
# Built application files
*.apk
*.ap_
+*.aab
# Files for the ART/Dalvik VM
*.dex
@@ -43,8 +44,9 @@ captures/
.idea/caches
# Keystore files
-# Uncomment the following line if you do not want to check your keystore files in.
+# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
+#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
index 000ee5f104b..9532800ba22 100644
--- a/vendor/gitignore/Delphi.gitignore
+++ b/vendor/gitignore/Delphi.gitignore
@@ -64,3 +64,6 @@ __recovery/
# Castalia statistics file (since XE7 Castalia is distributed with Delphi)
*.stat
+
+# Boss dependency manager vendor folder https://github.com/HashLoad/boss
+modules/
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index 86e4c3f3905..b263cd10f37 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -7,3 +7,4 @@ erl_crash.dump
*.ez
*.beam
/config/*.secret.exs
+.elixir_ls/
diff --git a/vendor/gitignore/Global/Images.gitignore b/vendor/gitignore/Global/Images.gitignore
new file mode 100644
index 00000000000..97dcdbe6a95
--- /dev/null
+++ b/vendor/gitignore/Global/Images.gitignore
@@ -0,0 +1,63 @@
+# JPEG
+*.jpg
+*.jpeg
+*.jpe
+*.jif
+*.jfif
+*.jfi
+
+# JPEG 2000
+*.jp2
+*.j2k
+*.jpf
+*.jpx
+*.jpm
+*.mj2
+
+# JPEG XR
+*.jxr
+*.hdp
+*.wdp
+
+# Graphics Interchange Format
+*.gif
+
+# RAW
+*.raw
+
+# Web P
+*.webp
+
+# Portable Network Graphics
+*.png
+
+# Animated Portable Network Graphics
+*.apng
+
+# Multiple-image Network Graphics
+*.mng
+
+# Tagged Image File Format
+*.tiff
+*.tif
+
+# Scalable Vector Graphics
+*.svg
+*.svgz
+
+# Portable Document Format
+*.pdf
+
+# X BitMap
+*.xbm
+
+# BMP
+*.bmp
+*.dib
+
+# ICO
+*.ico
+
+# 3D Images
+*.3dm
+*.max
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
index 254108cd23b..863bc7fa66e 100644
--- a/vendor/gitignore/Global/NetBeans.gitignore
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -1,4 +1,4 @@
-nbproject/private/
+**/nbproject/private/
build/
nbbuild/
dist/
diff --git a/vendor/gitignore/Global/PSoCCreator.gitignore b/vendor/gitignore/Global/PSoCCreator.gitignore
new file mode 100644
index 00000000000..15ae040bcda
--- /dev/null
+++ b/vendor/gitignore/Global/PSoCCreator.gitignore
@@ -0,0 +1,18 @@
+# Project Settings
+*.cywrk.*
+*.cyprj.*
+
+# Generated Assets and Resources
+Debug/
+Release/
+Export/
+*/codegentemp
+*/Generated_Source
+*_datasheet.pdf
+*_timing.html
+*.cycdx
+*.cyfit
+*.rpt
+*.svd
+*.log
+*.zip
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index cd0c7d3e45a..b01314d3a64 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,17 +2,11 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## User settings
-xcuserdata/
-
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+## Build generated
build/
DerivedData/
-*.moved-aside
+
+## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -21,3 +15,65 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index 67e2146f2bc..6552ddf8a06 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,4 +1,4 @@
-vendor/
+/vendor/
node_modules/
npm-debug.log
yarn-error.log
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 6f1fa223992..abe6d79fedb 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -2,6 +2,8 @@
# Magento Default Files #
#--------------------------#
+/PATCH_*.sh
+
/app/etc/local.xml
/media/*
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index c221276ebae..e1da6ae8ea5 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -71,3 +71,6 @@ typings/
# Serverless directories
.serverless
+
+# FuseBox cache
+.fusebox/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 6f7a6d9c3d7..510c73d0fdb 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -109,3 +109,6 @@ venv.bak/
.mypy_cache/
.dmypy.json
dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 78eb74fdc26..38ba1b5b38c 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -50,6 +50,7 @@ node_modules/
# Ignore precompiled javascript packs
/public/packs
/public/packs-test
+/public/assets
# Ignore yarn files
/yarn-error.log
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 0210746b1a5..833e6d4291c 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -23,6 +23,7 @@ ExportedObj/
*.svd
*.pdb
*.opendb
+*.VC.db
# Unity3D generated meta files
*.pidb.meta
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 5a7f7c0ebd1..ea3d3fd02f9 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -67,8 +67,9 @@
@babel/template,7.1.2,MIT
@babel/traverse,7.1.0,MIT
@babel/types,7.1.2,MIT
+@gitlab-org/gitlab-svgs,1.32.0,MIT
+@gitlab-org/gitlab-ui,1.10.0,MIT
@gitlab/svgs,1.35.0,MIT
-@gitlab-org/gitlab-ui,1.8.0,MIT
@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
@vue/component-compiler-utils,2.2.0,MIT
@@ -133,7 +134,6 @@ asciidoctor,1.5.6.2,MIT
asciidoctor-plantuml,0.0.8,MIT
asn1.js,4.10.1,MIT
assert,1.4.1,MIT
-asset_sync,2.4.0,MIT
assign-symbols,1.0.0,MIT
async-each,1.0.1,MIT
async-limiter,1.0.0,MIT
@@ -161,7 +161,6 @@ big.js,3.2.0,MIT
binary-extensions,1.11.0,MIT
binaryextensions,2.1.1,MIT
bindata,2.4.3,ruby
-blackst0ne-mermaid,7.1.0-fixed,MIT
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -264,8 +263,8 @@ css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
cyclist,0.2.2,MIT*
-d3,3.5.17,New BSD
d3,4.12.2,New BSD
+d3,4.13.0,New BSD
d3-array,1.2.1,New BSD
d3-axis,1.0.8,New BSD
d3-brush,1.0.4,New BSD
@@ -278,6 +277,7 @@ d3-dsv,1.0.8,New BSD
d3-ease,1.0.3,New BSD
d3-force,1.1.0,New BSD
d3-format,1.2.1,New BSD
+d3-format,1.2.2,New BSD
d3-geo,1.9.1,New BSD
d3-hierarchy,1.1.5,New BSD
d3-interpolate,1.1.6,New BSD
@@ -289,6 +289,7 @@ d3-random,1.1.0,New BSD
d3-request,1.0.6,New BSD
d3-scale,1.0.7,New BSD
d3-selection,1.2.0,New BSD
+d3-selection,1.3.0,New BSD
d3-shape,1.2.0,New BSD
d3-time,1.0.8,New BSD
d3-time-format,2.1.1,New BSD
@@ -296,8 +297,8 @@ d3-timer,1.0.7,New BSD
d3-transition,1.1.1,New BSD
d3-voronoi,1.1.2,New BSD
d3-zoom,1.7.1,New BSD
-dagre-d3-renderer,0.4.24,MIT
-dagre-layout,0.8.0,MIT
+dagre-d3-renderer,0.5.8,MIT
+dagre-layout,0.8.8,MIT
date-now,0.1.4,MIT
dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
@@ -328,7 +329,6 @@ device_detector,1.0.0,LGPL
devise,4.4.3,MIT
devise-two-factor,3.0.0,MIT
diff,3.5.0,New BSD
-diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+"
diffie-hellman,5.0.2,MIT
diffy,3.1.0,MIT
document-register-element,1.3.0,MIT
@@ -370,6 +370,7 @@ es6-promise,3.0.2,MIT
escape-html,1.0.3,MIT
escape-string-regexp,1.0.5,MIT
escape_utils,1.1.1,MIT
+escaper,2.5.3,MIT
eslint-scope,4.0.0,Simplified BSD
esrecurse,4.2.1,Simplified BSD
estraverse,4.2.0,Simplified BSD
@@ -447,12 +448,8 @@ get-value,2.0.6,MIT
get_process_mem,0.2.0,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.118.1,MIT
-github-linguist,5.3.3,MIT
+gitaly-proto,0.123.0,MIT
github-markup,1.7.0,MIT
-gitlab-flowdock-git-hook,1.0.1,MIT
-gitlab-gollum-lib,4.2.7.5,MIT
-gitlab-grit,2.8.2,MIT
gitlab-markup,1.6.4,MIT
gitlab-sidekiq-fetcher,0.3.0,LGPL
gitlab_omniauth-ldap,2.0.4,MIT
@@ -461,13 +458,12 @@ glob-parent,3.1.0,ISC
global-modules-path,2.1.0,Apache 2.0
globalid,0.4.1,MIT
globals,11.7.0,MIT
-gollum-grit_adapter,1.0.1,MIT
gon,6.2.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.23.4,Apache 2.0
-google-protobuf,3.5.1,New BSD
-googleapis-common-protos-types,1.0.1,Apache 2.0
-googleauth,0.6.2,Apache 2.0
+google-protobuf,3.6.1,New BSD
+googleapis-common-protos-types,1.0.2,Apache 2.0
+googleauth,0.6.6,Apache 2.0
got,8.3.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
@@ -476,9 +472,9 @@ grape-entity,0.7.1,MIT
grape-path-helpers,1.0.6,MIT
grape_logging,1.7.0,MIT
graphiql-rails,1.4.10,MIT
-graphlib,2.1.1,MIT
+graphlibrary,2.2.0,MIT
graphql,1.8.1,MIT
-grpc,1.11.0,Apache 2.0
+grpc,1.15.0,Apache 2.0
gzip-size,5.0.0,MIT
hamlit,2.8.8,MIT
hangouts-chat,0.0.5,MIT
@@ -572,6 +568,7 @@ is-plain-obj,1.1.0,MIT
is-plain-object,2.0.4,MIT
is-promise,2.1.0,MIT
is-regex,1.0.4,MIT
+is-regexp,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
is-stream,1.1.0,MIT
is-symbol,1.0.2,MIT
@@ -620,7 +617,6 @@ lazy-cache,2.0.2,MIT
lcid,2.0.0,MIT
licensee,8.9.2,MIT
lie,3.1.1,MIT
-little-plugger,1.1.4,MIT
loader-runner,2.3.0,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
@@ -635,7 +631,6 @@ lodash.get,4.4.2,MIT
lodash.isequal,4.5.0,MIT
lodash.mergewith,4.6.0,MIT
lodash.startcase,4.4.0,MIT
-logging,2.2.2,MIT
lograge,0.10.0,MIT
loofah,2.2.2,MIT
loose-envify,1.4.0,MIT
@@ -658,6 +653,7 @@ memoist,0.16.0,MIT
memory-fs,0.4.1,MIT
merge-descriptors,1.0.1,MIT
merge-source-map,1.1.0,MIT
+mermaid,8.0.0-rc.8,MIT
method_source,0.9.0,MIT
methods,1.1.2,MIT
micromatch,3.1.10,MIT
@@ -685,11 +681,10 @@ mississippi,2.0.0,Simplified BSD
mississippi,3.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
-moment,2.19.2,MIT
+moment,2.22.2,MIT
monaco-editor,0.14.3,MIT
monaco-editor-webpack-plugin,1.5.4,MIT
mousetrap,1.4.6,Apache 2.0
-mousetrap-rails,1.4.6,"MIT,Apache"
move-concurrently,1.0.1,ISC
ms,2.0.0,MIT
ms,2.1.1,MIT
@@ -760,7 +755,7 @@ opener,1.5.1,(WTFPL OR MIT)
opn,4.0.2,MIT
org-ruby,0.9.12,MIT
orm_adapter,0.5.0,MIT
-os,0.9.6,MIT
+os,1.0.0,MIT
os-browserify,0.3.0,MIT
os-homedir,1.0.2,MIT
os-locale,3.0.1,MIT
@@ -806,7 +801,6 @@ pkg-dir,3.0.0,MIT
po_to_json,1.0.1,MIT
popper.js,1.14.3,MIT
posix-character-classes,0.1.1,MIT
-posix-spawn,0.3.13,MIT
postcss,6.0.23,MIT
postcss-modules-extract-imports,1.2.0,ISC
postcss-modules-local-by-default,1.2.0,MIT
@@ -830,6 +824,8 @@ prr,1.0.1,MIT
pseudomap,1.0.2,ISC
public-encrypt,4.0.0,MIT
public_suffix,3.0.3,MIT
+puma,3.12.0,New BSD
+puma_worker_killer,0.1.0,MIT
pump,2.0.1,MIT
pump,3.0.0,MIT
pumpify,1.4.0,MIT
@@ -929,7 +925,7 @@ ruby_parser,3.9.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.27.4,MIT
+rugged,0.27.5,MIT
run-async,2.3.0,MIT
run-queue,1.0.3,ISC
rw,1.3.3,New BSD
@@ -949,6 +945,7 @@ sawyer,0.8.1,MIT
sax,1.2.4,ISC
schema-utils,0.4.5,MIT
schema-utils,1.0.0,MIT
+scope-css,1.2.1,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
select2,3.5.2-browserify,Apache*
@@ -975,8 +972,9 @@ shebang-regex,1.0.0,MIT
sidekiq,5.2.1,LGPL
sidekiq-cron,0.6.0,MIT
signal-exit,3.0.2,ISC
-signet,0.8.1,Apache 2.0
+signet,0.11.0,Apache 2.0
slack-notifier,1.5.1,MIT
+slugify,1.3.1,MIT
smooshpack,0.0.48,LGPL
snapdragon,0.8.1,MIT
snapdragon-node,2.1.1,MIT
@@ -1012,9 +1010,9 @@ string-width,1.0.2,MIT
string-width,2.1.1,MIT
string_decoder,0.10.31,MIT
string_decoder,1.1.1,MIT
-stringex,2.8.4,MIT
strip-ansi,3.0.1,MIT
strip-ansi,4.0.0,MIT
+strip-css-comments,3.0.0,MIT
strip-eof,1.0.0,MIT
strip-json-comments,2.0.1,MIT
style-loader,0.23.0,MIT