summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorConstance Okoghenun <constanceokoghenun@gmail.com>2018-11-05 10:33:05 +0100
committerConstance Okoghenun <constanceokoghenun@gmail.com>2018-11-05 10:33:05 +0100
commit3bac1a322c82dd9b6e9b23edd66fa45afaa9859f (patch)
treed3e7d47a3e0c8047cb2972aefbc12fe1e3080ef8
parentfe7b6f57120946a5d5fe4a4d54a245dc76b06dc8 (diff)
parent9e2eb85e365e2a33e52e3f1f48cc23ad4201a52b (diff)
downloadgitlab-ce-issue_51323.tar.gz
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into issue_51323issue_51323
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab-ci.yml10
-rw-r--r--CHANGELOG.md89
-rw-r--r--Dockerfile.assets4
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock4
-rw-r--r--Gemfile.rails5.lock2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board.js27
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue128
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js41
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue256
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue80
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue229
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue198
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue30
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js43
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue122
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js9
-rw-r--r--app/assets/javascripts/boards/index.js6
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js10
-rw-r--r--app/assets/javascripts/boards/models/issue.js31
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/services/board_service.js12
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js57
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js12
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue19
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js17
-rw-r--r--app/assets/javascripts/diffs/components/app.vue10
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue12
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue12
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue4
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue152
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js30
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js27
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js113
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js21
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js74
-rw-r--r--app/assets/javascripts/gl_field_errors.js2
-rw-r--r--app/assets/javascripts/group.js12
-rw-r--r--app/assets/javascripts/ide/components/ide.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue6
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue10
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue9
-rw-r--r--app/assets/javascripts/jobs/components/erased_block.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue292
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue12
-rw-r--r--app/assets/javascripts/jobs/components/job_log.vue74
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue42
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue9
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue8
-rw-r--r--app/assets/javascripts/jobs/index.js1
-rw-r--r--app/assets/javascripts/jobs/store/getters.js9
-rw-r--r--app/assets/javascripts/labels_select.js248
-rw-r--r--app/assets/javascripts/lib/utils/ace_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/members.js10
-rw-r--r--app/assets/javascripts/milestone_select.js5
-rw-r--r--app/assets/javascripts/notes.js6
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue2
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js8
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue99
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue348
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue5
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js38
-rw-r--r--app/assets/javascripts/projects/project_new.js44
-rw-r--r--app/assets/javascripts/right_sidebar.js57
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue124
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue56
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue101
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_links.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue143
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue4
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js9
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss95
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss78
-rw-r--r--app/controllers/admin/appearances_controller.rb6
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/controllers/clusters/applications_controller.rb28
-rw-r--r--app/controllers/clusters/base_controller.rb37
-rw-r--r--app/controllers/clusters/clusters_controller.rb218
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/concerns/project_unauthorized.rb10
-rw-r--r--app/controllers/concerns/routable_actions.rb16
-rw-r--r--app/controllers/dashboard/milestones_controller.rb7
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb26
-rw-r--r--app/controllers/projects/clusters_controller.rb222
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/controllers/projects/git_http_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb19
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb29
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/autocomplete/users_finder.rb2
-rw-r--r--app/finders/clusters_finder.rb8
-rw-r--r--app/finders/concerns/finder_with_cross_project_access.rb2
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/groups_finder.rb4
-rw-r--r--app/finders/issuable_finder.rb73
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/labels_finder.rb2
-rw-r--r--app/finders/milestones_finder.rb2
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/finders/pipelines_finder.rb2
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/helpers/blob_helper.rb5
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/mailers/emails/issues.rb14
-rw-r--r--app/mailers/emails/merge_requests.rb14
-rw-r--r--app/mailers/previews/notify_preview.rb20
-rw-r--r--app/models/blob.rb11
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/clusters/cluster.rb36
-rw-r--r--app/models/clusters/group.rb10
-rw-r--r--app/models/clusters/kubernetes_namespace.rb35
-rw-r--r--app/models/clusters/platforms/kubernetes.rb36
-rw-r--r--app/models/commit_status.rb7
-rw-r--r--app/models/concerns/awardable.rb18
-rw-r--r--app/models/concerns/blob_language_from_git_attributes.rb13
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/deployment_platform.rb1
-rw-r--r--app/models/concerns/fast_destroy_all.rb2
-rw-r--r--app/models/concerns/issuable.rb5
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/redactable.rb33
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb2
-rw-r--r--app/models/concerns/token_authenticatable.rb60
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb73
-rw-r--r--app/models/concerns/token_authenticatable_strategies/digest.rb50
-rw-r--r--app/models/concerns/token_authenticatable_strategies/insecure.rb23
-rw-r--r--app/models/concerns/with_uploads.rb2
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/diff_note.rb6
-rw-r--r--app/models/discussion_note.rb6
-rw-r--r--app/models/environment_status.rb41
-rw-r--r--app/models/global_milestone.rb44
-rw-r--r--app/models/group.rb5
-rw-r--r--app/models/issue.rb3
-rw-r--r--app/models/key.rb4
-rw-r--r--app/models/lfs_object.rb4
-rw-r--r--app/models/merge_request.rb15
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/milestone.rb18
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/note.rb3
-rw-r--r--app/models/personal_access_token.rb16
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb7
-rw-r--r--app/models/snippet.rb3
-rw-r--r--app/models/user.rb24
-rw-r--r--app/presenters/blob_presenter.rb16
-rw-r--r--app/presenters/clusterable_presenter.rb46
-rw-r--r--app/presenters/clusters/cluster_presenter.rb8
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/presenters/merge_request_presenter.rb12
-rw-r--r--app/presenters/project_clusterable_presenter.rb15
-rw-r--r--app/presenters/project_presenter.rb2
-rw-r--r--app/serializers/build_action_entity.rb7
-rw-r--r--app/serializers/environment_status_entity.rb1
-rw-r--r--app/serializers/job_entity.rb17
-rw-r--r--app/serializers/merge_request_widget_entity.rb1
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/clusters/create_service.rb22
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb23
-rw-r--r--app/services/clusters/gcp/kubernetes.rb11
-rw-r--r--app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb54
-rw-r--r--app/services/clusters/gcp/kubernetes/create_service_account_service.rb78
-rw-r--r--app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb8
-rw-r--r--app/services/delete_merged_branches_service.rb4
-rw-r--r--app/services/issuable_base_service.rb8
-rw-r--r--app/services/issues/update_service.rb14
-rw-r--r--app/services/keys/destroy_service.rb2
-rw-r--r--app/services/labels/transfer_service.rb2
-rw-r--r--app/services/members/base_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb4
-rw-r--r--app/services/merge_requests/merge_service.rb5
-rw-r--r--app/services/merge_requests/refresh_service.rb18
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb5
-rw-r--r--app/services/merge_requests/update_service.rb14
-rw-r--r--app/services/milestones/destroy_service.rb2
-rw-r--r--app/services/notification_service.rb41
-rw-r--r--app/services/projects/move_project_authorizations_service.rb2
-rw-r--r--app/services/projects/move_project_group_links_service.rb2
-rw-r--r--app/services/projects/move_project_members_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/views/ci/variables/_index.html.haml4
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml (renamed from app/views/projects/clusters/_advanced_settings.html.haml)2
-rw-r--r--app/views/clusters/clusters/_banner.html.haml (renamed from app/views/projects/clusters/_banner.html.haml)0
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml (renamed from app/views/projects/clusters/_cluster.html.haml)4
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml (renamed from app/views/projects/clusters/_empty_state.html.haml)4
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml (renamed from app/views/projects/clusters/_gcp_signup_offer_banner.html.haml)0
-rw-r--r--app/views/clusters/clusters/_integration_form.html.haml (renamed from app/views/projects/clusters/_integration_form.html.haml)6
-rw-r--r--app/views/clusters/clusters/_sidebar.html.haml (renamed from app/views/projects/clusters/_sidebar.html.haml)0
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml (renamed from app/views/projects/clusters/gcp/_form.html.haml)4
-rw-r--r--app/views/clusters/clusters/gcp/_header.html.haml (renamed from app/views/projects/clusters/gcp/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml (renamed from app/views/projects/clusters/gcp/_show.html.haml)2
-rw-r--r--app/views/clusters/clusters/index.html.haml (renamed from app/views/projects/clusters/index.html.haml)0
-rw-r--r--app/views/clusters/clusters/new.html.haml (renamed from app/views/projects/clusters/new.html.haml)8
-rw-r--r--app/views/clusters/clusters/show.html.haml (renamed from app/views/projects/clusters/show.html.haml)21
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml (renamed from app/views/projects/clusters/user/_form.html.haml)4
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml (renamed from app/views/projects/clusters/user/_header.html.haml)0
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml (renamed from app/views/projects/clusters/user/_show.html.haml)2
-rw-r--r--app/views/groups/new.html.haml33
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/notify/changed_milestone_issue_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_issue_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.html.haml2
-rw-r--r--app/views/notify/removed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_highlight_embed.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_text.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml6
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml1
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml33
-rw-r--r--app/views/projects/pipelines/show.html.haml10
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml4
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml6
-rw-r--r--app/views/shared/_group_form.html.haml45
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml15
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/sherlock/queries/_general.html.haml2
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/cluster_platform_configure_worker.rb22
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/post_receive.rb13
-rw-r--r--changelogs/unreleased/28249-add-pagination.yml5
-rw-r--r--changelogs/unreleased/34758-create-group-clusters.yml5
-rw-r--r--changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml5
-rw-r--r--changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml5
-rw-r--r--changelogs/unreleased/44012-filter-reactions-none-any.yml5
-rw-r--r--changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml5
-rw-r--r--changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml5
-rw-r--r--changelogs/unreleased/51259-ci-cd-gitlab-ui.yml5
-rw-r--r--changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml5
-rw-r--r--changelogs/unreleased/51527-xss-in-mr-source-branch.yml5
-rw-r--r--changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml4
-rw-r--r--changelogs/unreleased/51716-create-kube-namespace.yml5
-rw-r--r--changelogs/unreleased/52122-fix-broken-whitespace-button.yml5
-rw-r--r--changelogs/unreleased/52382-filter-milestone-api-none-any.yml5
-rw-r--r--changelogs/unreleased/52383-ui-filter-assignee-none-any.yml5
-rw-r--r--changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml5
-rw-r--r--changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml5
-rw-r--r--changelogs/unreleased/53052-mg-fix-broken-ie11.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/53155-structured-logs-params-array.yml5
-rw-r--r--changelogs/unreleased/53227-empty-list.yml6
-rw-r--r--changelogs/unreleased/53270-remove-mousetrap-rails.yml5
-rw-r--r--changelogs/unreleased/53273-update-moment-to-2-22-2.yml5
-rw-r--r--changelogs/unreleased/ab-45608-stuck-mr-query.yml5
-rw-r--r--changelogs/unreleased/ac-post-merge-pipeline.yml5
-rw-r--r--changelogs/unreleased/add-failure-reason-for-execution-timeout.yml5
-rw-r--r--changelogs/unreleased/add-scheduled-flag-to-job-entity.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml5
-rw-r--r--changelogs/unreleased/ccr-51520_change_milestone_email.yml5
-rw-r--r--changelogs/unreleased/drop-gcp-cluster-table.yml5
-rw-r--r--changelogs/unreleased/fix-53298.yml5
-rw-r--r--changelogs/unreleased/fl-missing-i18n.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml5
-rw-r--r--changelogs/unreleased/gl-ui-modal.yml5
-rw-r--r--changelogs/unreleased/gl-ui-pagination.yml5
-rw-r--r--changelogs/unreleased/gl-ui-progress-bar.yml5
-rw-r--r--changelogs/unreleased/gt-fix-ide-typos-in-props.yml5
-rw-r--r--changelogs/unreleased/gt-fix-quick-links-button-styles.yml5
-rw-r--r--changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml5
-rw-r--r--changelogs/unreleased/kinolaev-master-patch-91872.yml5
-rw-r--r--changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml5
-rw-r--r--changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml5
-rw-r--r--changelogs/unreleased/rails5-deprecated-uniq.yml5
-rw-r--r--changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml5
-rw-r--r--changelogs/unreleased/redact-links-dev.yml5
-rw-r--r--changelogs/unreleased/related_mrs.yml5
-rw-r--r--changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml5
-rw-r--r--changelogs/unreleased/replace-tooltip-in-markdown-component.yml5
-rw-r--r--changelogs/unreleased/rz_fix_milestone_count.yml5
-rw-r--r--changelogs/unreleased/security-2717-fix-issue-title-xss.yml5
-rw-r--r--changelogs/unreleased/security-51113-hash_personal_access_tokens.yml5
-rw-r--r--changelogs/unreleased/security-kubeclient-ssrf.yml5
-rw-r--r--changelogs/unreleased/sh-fix-hipchat-ssrf.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-53153.yml5
-rw-r--r--changelogs/unreleased/sh-fix-search-relative-urls.yml5
-rw-r--r--changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-reload-diffs-service.yml5
-rw-r--r--changelogs/unreleased/tc-index-lfs-objects-file-store.yml5
-rw-r--r--changelogs/unreleased/toggle-sidebar-alignment.yml5
-rw-r--r--changelogs/unreleased/top_level_clusters_controller.yml6
-rw-r--r--changelogs/unreleased/update_license_management_job.yml5
-rw-r--r--changelogs/unreleased/winh-job-list-dynamic-timer.yml5
-rw-r--r--changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml5
-rw-r--r--config/application.rb2
-rw-r--r--config/dependency_decisions.yml2
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/8_metrics.rb6
-rw-r--r--config/initializers/hipchat_client_patch.rb14
-rw-r--r--config/initializers/kubeclient.rb21
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb18
-rw-r--r--config/routes/project.rb20
-rw-r--r--db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb4
-rw-r--r--db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb19
-rw-r--r--db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb17
-rw-r--r--db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb17
-rw-r--r--db/migrate/20181014203236_create_cluster_groups.rb17
-rw-r--r--db/migrate/20181017001059_add_cluster_type_to_clusters.rb18
-rw-r--r--db/migrate/20181031190559_drop_gcp_clusters_table.rb53
-rw-r--r--db/migrate/20181101144347_add_index_for_stuck_mr_query.rb16
-rw-r--r--db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb2
-rw-r--r--db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb28
-rw-r--r--db/post_migrate/20181014121030_enqueue_redact_links.rb65
-rw-r--r--db/schema.rb51
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/high_availability/nfs.md19
-rw-r--r--doc/administration/high_availability/redis.md8
-rw-r--r--doc/administration/high_availability/redis_source.md4
-rw-r--r--doc/administration/logs.md2
-rw-r--r--doc/api/issues.md99
-rw-r--r--doc/api/merge_requests.md34
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md4
-rw-r--r--doc/ci/examples/container_scanning.md6
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md6
-rw-r--r--doc/ci/services/mysql.md2
-rw-r--r--doc/development/api_graphql_styleguide.md2
-rw-r--r--doc/development/contributing/merge_request_workflow.md3
-rw-r--r--doc/development/fe_guide/icons.md4
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md33
-rw-r--r--doc/development/testing_guide/review_apps.md6
-rw-r--r--doc/development/ux_guide/tips.md18
-rw-r--r--doc/install/digitaloceandocker.md102
-rw-r--r--doc/raketasks/backup_restore.md315
-rw-r--r--doc/ssh/README.md243
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/autodevops/index.md13
-rw-r--r--doc/topics/autodevops/quick_start_guide.md3
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/university/training/user_training.md267
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md6
-rw-r--r--doc/user/markdown.md111
-rw-r--r--doc/user/project/clusters/index.md8
-rw-r--r--doc/user/project/clusters/runbooks/index.md49
-rw-r--r--doc/user/project/import/index.md3
-rw-r--r--doc/user/project/issue_board.md4
-rw-r--r--doc/user/project/merge_requests/index.md29
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--doc/user/search/img/issues_filter_none_any.pngbin0 -> 27717 bytes
-rw-r--r--doc/user/search/index.md10
-rw-r--r--doc/workflow/notifications.md45
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/issues.rb41
-rw-r--r--lib/api/validations/types/safe_file.rb15
-rw-r--r--lib/api/wikis.rb4
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb11
-rw-r--r--lib/banzai/issuable_extractor.rb41
-rw-r--r--lib/extracts_path.rb2
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb4
-rw-r--r--lib/gitlab/background_migration/digest_column.rb25
-rw-r--r--lib/gitlab/background_migration/redact_links.rb62
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_services.rb4
-rw-r--r--lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb4
-rw-r--r--lib/gitlab/blame.rb3
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb8
-rw-r--r--lib/gitlab/checks/change_access.rb110
-rw-r--r--lib/gitlab/checks/lfs_integrity.rb5
-rw-r--r--lib/gitlab/checks/timed_logger.rb83
-rw-r--r--lib/gitlab/ci/ansi2html.rb4
-rw-r--r--lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/path.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/base.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/factory.rb2
-rw-r--r--lib/gitlab/ci/build/credentials/registry.rb2
-rw-r--r--lib/gitlab/ci/build/image.rb2
-rw-r--r--lib/gitlab/ci/build/policy.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/step.rb2
-rw-r--r--lib/gitlab/ci/charts.rb2
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb2
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb2
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb2
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb2
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb2
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb2
-rw-r--r--lib/gitlab/ci/config/entry/global.rb2
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb2
-rw-r--r--lib/gitlab/ci/config/entry/image.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb2
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb2
-rw-r--r--lib/gitlab/ci/config/entry/key.rb2
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb2
-rw-r--r--lib/gitlab/ci/config/entry/node.rb2
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb2
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb2
-rw-r--r--lib/gitlab/ci/config/entry/script.rb2
-rw-r--r--lib/gitlab/ci/config/entry/service.rb2
-rw-r--r--lib/gitlab/ci/config/entry/services.rb2
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb2
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb2
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb2
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb2
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb2
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb2
-rw-r--r--lib/gitlab/ci/config/loader.rb2
-rw-r--r--lib/gitlab/ci/cron_parser.rb2
-rw-r--r--lib/gitlab/ci/mask_secret.rb4
-rw-r--r--lib/gitlab/ci/model.rb2
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb5
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/repository.rb2
-rw-r--r--lib/gitlab/ci/pipeline/duration.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/equals.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/null.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/operator.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/value.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/variable.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/token.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb2
-rw-r--r--lib/gitlab/ci/reports/test_case.rb2
-rw-r--r--lib/gitlab/ci/reports/test_reports.rb2
-rw-r--r--lib/gitlab/ci/reports/test_reports_comparer.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb2
-rw-r--r--lib/gitlab/ci/reports/test_suite_comparer.rb2
-rw-r--r--lib/gitlab/ci/status/build/action.rb2
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/build/common.rb2
-rw-r--r--lib/gitlab/ci/status/build/created.rb2
-rw-r--r--lib/gitlab/ci/status/build/erased.rb2
-rw-r--r--lib/gitlab/ci/status/build/factory.rb2
-rw-r--r--lib/gitlab/ci/status/build/failed.rb5
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/build/manual.rb2
-rw-r--r--lib/gitlab/ci/status/build/pending.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retried.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/scheduled.rb2
-rw-r--r--lib/gitlab/ci/status/build/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/build/unschedule.rb2
-rw-r--r--lib/gitlab/ci/status/canceled.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb2
-rw-r--r--lib/gitlab/ci/status/created.rb2
-rw-r--r--lib/gitlab/ci/status/extended.rb2
-rw-r--r--lib/gitlab/ci/status/external/common.rb2
-rw-r--r--lib/gitlab/ci/status/external/factory.rb2
-rw-r--r--lib/gitlab/ci/status/factory.rb2
-rw-r--r--lib/gitlab/ci/status/failed.rb2
-rw-r--r--lib/gitlab/ci/status/group/common.rb2
-rw-r--r--lib/gitlab/ci/status/group/factory.rb2
-rw-r--r--lib/gitlab/ci/status/manual.rb2
-rw-r--r--lib/gitlab/ci/status/pending.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/blocked.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/common.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/delayed.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb2
-rw-r--r--lib/gitlab/ci/status/running.rb2
-rw-r--r--lib/gitlab/ci/status/scheduled.rb2
-rw-r--r--lib/gitlab/ci/status/skipped.rb2
-rw-r--r--lib/gitlab/ci/status/stage/common.rb2
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb2
-rw-r--r--lib/gitlab/ci/status/success.rb2
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml15
-rw-r--r--lib/gitlab/ci/templates/Maven.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/trace.rb2
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb15
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb2
-rw-r--r--lib/gitlab/ci/trace/stream.rb5
-rw-r--r--lib/gitlab/ci/variables/collection.rb2
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb6
-rw-r--r--lib/gitlab/conflict/file.rb36
-rw-r--r--lib/gitlab/contributions_calendar.rb2
-rw-r--r--lib/gitlab/crypto_helper.rb30
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/diff/file_collection/base.rb7
-rw-r--r--lib/gitlab/diff/highlight.rb2
-rw-r--r--lib/gitlab/diff/position_tracer.rb2
-rw-r--r--lib/gitlab/git/blob.rb3
-rw-r--r--lib/gitlab/git/commit.rb9
-rw-r--r--lib/gitlab/git/commit_stats.rb4
-rw-r--r--lib/gitlab/git/conflict/resolver.rb6
-rw-r--r--lib/gitlab/git/lfs_changes.rb4
-rw-r--r--lib/gitlab/git/remote_mirror.rb4
-rw-r--r--lib/gitlab/git/repository.rb21
-rw-r--r--lib/gitlab/git/tree.rb3
-rw-r--r--lib/gitlab/git/wiki.rb18
-rw-r--r--lib/gitlab/git/wraps_gitaly_errors.rb15
-rw-r--r--lib/gitlab/git_access.rb22
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb17
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb2
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb10
-rw-r--r--lib/gitlab/highlight.rb17
-rw-r--r--lib/gitlab/identifier.rb21
-rw-r--r--lib/gitlab/import/merge_request_helpers.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/kubernetes/helm.rb1
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb6
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb2
-rw-r--r--lib/gitlab/kubernetes/role_binding.rb11
-rw-r--r--lib/gitlab/path_regex.rb2
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/proxy_http_connection_adapter.rb2
-rw-r--r--lib/gitlab/search_results.rb17
-rw-r--r--lib/gitlab/slash_commands/issue_new.rb2
-rw-r--r--lib/gitlab/url_blocker.rb7
-rw-r--r--lib/gitlab/user_extractor.rb4
-rw-r--r--lib/google_api/auth.rb2
-rw-r--r--lib/tasks/tokens.rake14
-rw-r--r--locale/gitlab.pot69
-rw-r--r--package.json4
-rw-r--r--qa/qa.rb5
-rw-r--r--qa/qa/factory/README.md57
-rw-r--r--qa/qa/factory/base.rb10
-rw-r--r--qa/qa/factory/product.rb35
-rw-r--r--qa/qa/factory/repository/project_push.rb2
-rw-r--r--qa/qa/factory/repository/push.rb2
-rw-r--r--qa/qa/factory/resource/ci_variable.rb (renamed from qa/qa/factory/resource/secret_variable.rb)8
-rw-r--r--qa/qa/factory/resource/fork.rb3
-rw-r--r--qa/qa/factory/resource/merge_request.rb6
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb4
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb3
-rw-r--r--qa/qa/factory/resource/project_milestone.rb3
-rw-r--r--qa/qa/factory/resource/sandbox.rb1
-rw-r--r--qa/qa/factory/resource/ssh_key.rb6
-rw-r--r--qa/qa/factory/resource/user.rb6
-rw-r--r--qa/qa/page/project/issue/show.rb29
-rw-r--r--qa/qa/page/project/operations/kubernetes/add.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb2
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb4
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb (renamed from qa/qa/page/project/settings/secret_variables.rb)6
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb34
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb32
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb (renamed from qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb)14
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb2
-rw-r--r--qa/spec/factory/base_spec.rb62
-rw-r--r--qa/spec/factory/product_spec.rb29
-rw-r--r--rubocop/cop/code_reuse/active_record.rb1
-rwxr-xr-xscripts/build_assets_image21
-rwxr-xr-xscripts/static-analysis1
-rwxr-xr-xscripts/trigger-build12
-rw-r--r--spec/controllers/application_controller_spec.rb26
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb9
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb5
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb2
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb5
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb2
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb52
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb1
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb34
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb52
-rw-r--r--spec/controllers/uploads_controller_spec.rb25
-rw-r--r--spec/factories/clusters/clusters.rb15
-rw-r--r--spec/factories/clusters/kubernetes_namespaces.rb16
-rw-r--r--spec/features/admin/admin_groups_spec.rb28
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/commits_spec.rb60
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb2
-rw-r--r--spec/features/dashboard/group_spec.rb8
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/explore/new_menu_spec.rb4
-rw-r--r--spec/features/groups/board_sidebar_spec.rb45
-rw-r--r--spec/features/groups/milestone_spec.rb6
-rw-r--r--spec/features/groups_spec.rb14
-rw-r--r--spec/features/import/manifest_import_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb12
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb20
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb17
-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_sees_deployment_widget_spec.rb19
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb4
-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/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb1
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb5
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb8
-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.rb18
-rw-r--r--spec/features/projects/merge_request_button_spec.rb8
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb32
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json1
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/helpers/blob_helper_spec.rb58
-rw-r--r--spec/helpers/labels_helper_spec.rb25
-rw-r--r--spec/javascripts/awards_handler_spec.js4
-rw-r--r--spec/javascripts/boards/board_list_spec.js86
-rw-r--r--spec/javascripts/boards/components/board_spec.js37
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js6
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js8
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js23
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js126
-rw-r--r--spec/javascripts/diffs/mock_data/merge_request_diffs.js42
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js1
-rw-r--r--spec/javascripts/environments/emtpy_state_spec.js23
-rw-r--r--spec/javascripts/environments/environment_item_spec.js56
-rw-r--r--spec/javascripts/environments/environments_app_spec.js89
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js104
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js18
-rw-r--r--spec/javascripts/job_spec.js265
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js4
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js36
-rw-r--r--spec/javascripts/jobs/store/getters_spec.js6
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js221
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js4
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js8
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js33
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js2
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js8
-rw-r--r--spec/javascripts/pipelines/pipelines_actions_spec.js6
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/pipelines/stage_spec.js2
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js84
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js162
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js79
-rw-r--r--spec/javascripts/vue_mr_widget/components/review_app_link_spec.js38
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js4
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js191
-rw-r--r--spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js30
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js12
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js2
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb2
-rw-r--r--spec/lib/container_registry/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/digest_column_spec.rb46
-rw-r--r--spec/lib/gitlab/background_migration/redact_links_spec.rb96
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb15
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb18
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb5
-rw-r--r--spec/lib/gitlab/checks/timed_logger_spec.rb63
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb2
-rw-r--r--spec/lib/gitlab/cross_project_access/check_info_spec.rb4
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb2
-rw-r--r--spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb28
-rw-r--r--spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb28
-rw-r--r--spec/lib/gitlab/git_access_spec.rb10
-rw-r--r--spec/lib/gitlab/gpg_spec.rb4
-rw-r--r--spec/lib/gitlab/highlight_spec.rb83
-rw-r--r--spec/lib/gitlab/identifier_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb3
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb22
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb2
-rw-r--r--spec/lib/microsoft_teams/notifier_spec.rb2
-rw-r--r--spec/migrations/enqueue_redact_links_spec.rb42
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb2
-rw-r--r--spec/migrations/schedule_digest_personal_access_tokens_spec.rb46
-rw-r--r--spec/models/ci/build_spec.rb32
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb10
-rw-r--r--spec/models/clusters/cluster_spec.rb68
-rw-r--r--spec/models/clusters/group_spec.rb8
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb85
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb59
-rw-r--r--spec/models/concerns/awardable_spec.rb18
-rw-r--r--spec/models/concerns/blob_language_from_git_attributes_spec.rb25
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/redactable_spec.rb69
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb272
-rw-r--r--spec/models/environment_status_spec.rb32
-rw-r--r--spec/models/global_milestone_spec.rb35
-rw-r--r--spec/models/group_spec.rb18
-rw-r--r--spec/models/lfs_object_spec.rb21
-rw-r--r--spec/models/merge_request_diff_spec.rb33
-rw-r--r--spec/models/merge_request_spec.rb64
-rw-r--r--spec/models/milestone_spec.rb37
-rw-r--r--spec/models/personal_access_token_spec.rb28
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb18
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb34
-rw-r--r--spec/models/project_wiki_spec.rb4
-rw-r--r--spec/models/upload_spec.rb2
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/presenters/blob_presenter_spec.rb44
-rw-r--r--spec/presenters/clusterable_presenter_spec.rb17
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb14
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb9
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb77
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb24
-rw-r--r--spec/requests/api/issues_spec.rb92
-rw-r--r--spec/requests/api/runner_spec.rb9
-rw-r--r--spec/requests/api/wikis_spec.rb10
-rw-r--r--spec/serializers/build_action_entity_spec.rb4
-rw-r--r--spec/serializers/environment_status_entity_spec.rb3
-rw-r--r--spec/serializers/job_entity_spec.rb1
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb34
-rw-r--r--spec/services/ci/process_build_service_spec.rb44
-rw-r--r--spec/services/ci/run_scheduled_build_service_spec.rb4
-rw-r--r--spec/services/clusters/create_service_spec.rb31
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb278
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb115
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb211
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb50
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb2
-rw-r--r--spec/services/clusters/update_service_spec.rb7
-rw-r--r--spec/services/groups/transfer_service_spec.rb4
-rw-r--r--spec/services/issues/update_service_spec.rb48
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb60
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb11
-rw-r--r--spec/services/merge_requests/update_service_spec.rb46
-rw-r--r--spec/services/notification_service_spec.rb160
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb4
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/features/variable_list_shared_examples.rb2
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb8
-rw-r--r--spec/support/helpers/project_forks_helper.rb2
-rw-r--r--spec/support/helpers/seed_helper.rb20
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb6
-rw-r--r--spec/support/shared_examples/services/boards/lists_move_service.rb12
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb2
-rw-r--r--spec/workers/cluster_platform_configure_worker_spec.rb33
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb9
-rw-r--r--spec/workers/post_receive_spec.rb133
-rw-r--r--vendor/jupyter/values.yaml2
-rw-r--r--vendor/licenses.csv2
-rw-r--r--vendor/prometheus/values.yaml4
-rw-r--r--yarn.lock25
885 files changed, 10932 insertions, 5357 deletions
diff --git a/.gitattributes b/.gitattributes
index f1c41c9bb76..7282c9e61b1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
Dangerfile gitlab-language=ruby
+db/schema.rb merge=merge_db_schema
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c3163b687b4..0e7a67f9cc1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -265,7 +265,7 @@ package-and-qa:
SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_SLUG
- # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
+ # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
@@ -694,7 +694,10 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1
dependencies: []
+ services:
+ - docker:stable-dind
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
@@ -703,18 +706,23 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true"
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
+ DOCKER_DRIVER: overlay2
+ DOCKER_HOST: tcp://docker:2375
script:
- date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date
- free -m
- bundle exec rake gitlab:assets:compile
+ - scripts/build_assets_image
artifacts:
name: webpack-report
expire_in: 31d
paths:
- webpack-report/
- public/assets/
+ tags:
+ - docker
karma:
<<: *dedicated-no-docs-pull-cache-job
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fc5b24aa39..6e5296e231e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,50 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.4.5 (2018-11-04)
+
+### Fixed (4 changes, 1 of them is from the community)
+
+- fix link to enable usage ping from convdev index. !22545 (Anand Capur)
+- Update gitlab-ui dependency to 1.8.0-hotfix.1 to fix IE11 bug.
+- Remove duplicate escape in job sidebar.
+- Fixed merge request fill tree toggling not respecting fluid width preference.
+
+### Other (1 change)
+
+- Fix stage dropdown not rendering in different languages.
+
+
+## 11.4.4 (2018-10-30)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
+## 11.4.3 (2018-10-26)
+
+- No changes.
+
+## 11.4.2 (2018-10-25)
+
+### Security (5 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2571
+- Persist only SHA digest of PersonalAccessToken#token.
+- Redact personal tokens in unsubscribe links.
+- Block loopback addresses in UrlBlocker.
+- Validate Wiki attachments are valid temporary files.
+
+
+## 11.4.1 (2018-10-23)
+
+### Security (2 changes)
+
+- Fix XSS in merge request source branch name.
+- Prevent SSRF attacks in HipChat integration.
+
+
## 11.4.0 (2018-10-22)
### Security (9 changes)
@@ -227,6 +271,29 @@ entry.
- Check frozen string in style builds. (gfyoung)
+## 11.3.9 (2018-10-31)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
+## 11.3.8 (2018-10-27)
+
+- No changes.
+
+## 11.3.7 (2018-10-26)
+
+### Security (6 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2557
+- Persist only SHA digest of PersonalAccessToken#token.
+- Fix XSS in merge request source branch name.
+- Redact personal tokens in unsubscribe links.
+- Prevent SSRF attacks in HipChat integration.
+- Validate Wiki attachments are valid temporary files.
+
+
## 11.3.6 (2018-10-17)
- No changes.
@@ -516,6 +583,28 @@ entry.
- Creates Vue component for artifacts block on job page.
+## 11.2.8 (2018-10-31)
+
+### Security (1 change)
+
+- Monkey kubeclient to not follow any redirects.
+
+
+## 11.2.7 (2018-10-27)
+
+- No changes.
+
+## 11.2.6 (2018-10-26)
+
+### Security (5 changes)
+
+- Escape entity title while autocomplete template rendering to prevent XSS. !2558
+- Fix XSS in merge request source branch name.
+- Redact personal tokens in unsubscribe links.
+- Persist only SHA digest of PersonalAccessToken#token.
+- Prevent SSRF attacks in HipChat integration.
+
+
## 11.2.5 (2018-10-05)
### Security (3 changes)
diff --git a/Dockerfile.assets b/Dockerfile.assets
new file mode 100644
index 00000000000..403d16cc4ab
--- /dev/null
+++ b/Dockerfile.assets
@@ -0,0 +1,4 @@
+# Simple container to store assets for later use
+FROM scratch
+ADD public/assets /assets/
+CMD /bin/true
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 6085e946503..f0bb29e7638 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.2.1
+1.3.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 9da0a092a0d..6da4de57dc6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-8.4.0 \ No newline at end of file
+8.4.1
diff --git a/Gemfile b/Gemfile
index d8c996cf6c4..6674edc1d0c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -244,9 +244,6 @@ gem 'rack-attack', '~> 4.4.1'
# Ace editor
gem 'ace-rails-ap', '~> 4.1.0'
-# Keyboard shortcuts
-gem 'mousetrap-rails', '~> 1.4.6'
-
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index e533b564d15..e755b0e0a8d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -463,7 +463,6 @@ GEM
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
@@ -547,7 +546,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.12.1)
- parser (2.5.1.2)
+ parser (2.5.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.0.1)
@@ -1046,7 +1045,6 @@ DEPENDENCIES
method_source (~> 0.8)
mini_magick
minitest (~> 5.7.0)
- mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 5.0)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index b24911f3bb2..6ae7444f18f 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -466,7 +466,6 @@ GEM
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
@@ -1055,7 +1054,6 @@ DEPENDENCIES
method_source (~> 0.8)
mini_magick
minitest (~> 5.7.0)
- mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 5.0)
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 3cc89ff1955..ec27ae8c291 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix');
- const filePath = editBlobForm.data('blobFilename')
+ const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 623cda5679a..fb6e5291a61 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -42,7 +42,7 @@ export default Vue.extend({
required: true,
},
},
- data () {
+ data() {
return {
detailIssue: boardsStore.detail,
filter: boardsStore.filter,
@@ -55,27 +55,26 @@ export default Vue.extend({
},
isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
- }
+ },
},
watch: {
filter: {
handler() {
this.list.page = 1;
- this.list.getIssues(true)
- .catch(() => {
- // TODO: handle request error
- });
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
},
deep: true,
- }
+ },
},
- mounted () {
+ mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
- onEnd: (e) => {
+ onEnd: e => {
sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore.moveList(list, order);
});
}
- }
+ },
});
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
- const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
+ const isCollapsed =
+ localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
@@ -107,7 +107,10 @@ export default Vue.extend({
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded);
+ localStorage.setItem(
+ `boards.${this.boardId}.${this.list.type}.expanded`,
+ this.list.isExpanded,
+ );
}
}
},
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 38aaec73d7d..561a4636ef5 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -32,18 +32,18 @@ export default {
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels
- gl.boardService.generateDefaultLists()
+ gl.boardService
+ .generateDefaultLists()
.then(res => res.data)
- .then((data) => {
- data.forEach((listObj) => {
+ .then(data => {
+ data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id;
list.label.id = listObj.label.id;
- list.getIssues()
- .catch(() => {
- // TODO: handle request error
- });
+ list.getIssues().catch(() => {
+ // TODO: handle request error
+ });
});
})
.catch(() => {
@@ -57,7 +57,6 @@ export default {
clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
},
};
-
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 843498f0d06..2f31316aa76 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,77 +1,77 @@
<script>
- /* eslint-disable vue/require-default-prop */
- import IssueCardInner from './issue_card_inner.vue';
- import eventHub from '../eventhub';
- import boardsStore from '../stores/boards_store';
+/* eslint-disable vue/require-default-prop */
+import IssueCardInner from './issue_card_inner.vue';
+import eventHub from '../eventhub';
+import boardsStore from '../stores/boards_store';
- export default {
- name: 'BoardsIssueCard',
- components: {
- IssueCardInner,
+export default {
+ name: 'BoardsIssueCard',
+ components: {
+ IssueCardInner,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
},
- props: {
- list: {
- type: Object,
- default: () => ({}),
- },
- issue: {
- type: Object,
- default: () => ({}),
- },
- issueLinkBase: {
- type: String,
- default: '',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- index: {
- type: Number,
- default: 0,
- },
- rootPath: {
- type: String,
- default: '',
- },
- groupId: {
- type: Number,
- },
+ issue: {
+ type: Object,
+ default: () => ({}),
},
- data() {
- return {
- showDetail: false,
- detailIssue: boardsStore.detail,
- };
+ issueLinkBase: {
+ type: String,
+ default: '',
},
- computed: {
- issueDetailVisible() {
- return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
- },
+ disabled: {
+ type: Boolean,
+ default: false,
},
- methods: {
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- if (e.target.classList.contains('js-no-trigger')) return;
+ index: {
+ type: Number,
+ default: 0,
+ },
+ rootPath: {
+ type: String,
+ default: '',
+ },
+ groupId: {
+ type: Number,
+ },
+ },
+ data() {
+ return {
+ showDetail: false,
+ detailIssue: boardsStore.detail,
+ };
+ },
+ computed: {
+ issueDetailVisible() {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ },
+ },
+ methods: {
+ mouseDown() {
+ this.showDetail = true;
+ },
+ mouseMove() {
+ this.showDetail = false;
+ },
+ showIssue(e) {
+ if (e.target.classList.contains('js-no-trigger')) return;
- if (this.showDetail) {
- this.showDetail = false;
+ if (this.showDetail) {
+ this.showDetail = false;
- if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
- eventHub.$emit('clearDetailIssue');
- } else {
- eventHub.$emit('newDetailIssue', this.issue);
- boardsStore.detail.list = this.list;
- }
+ if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
+ eventHub.$emit('clearDetailIssue');
+ } else {
+ eventHub.$emit('newDetailIssue', this.issue);
+ boardsStore.detail.list = this.list;
}
- },
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 030288a1c9d..ee3dc38bca6 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { Button } from '@gitlab-org/gitlab-ui';
+import { GlButton } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -10,7 +10,7 @@ export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
- 'gl-button': Button,
+ GlButton,
},
props: {
groupId: {
@@ -62,7 +62,8 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
- return this.list.newIssue(issue)
+ return this.list
+ .newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 62666954de0..e637e1f1223 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -38,7 +38,7 @@ export default Vue.extend({
};
},
computed: {
- showSidebar () {
+ showSidebar() {
return Object.keys(this.issue).length;
},
milestoneTitle() {
@@ -51,18 +51,20 @@ export default Vue.extend({
return this.issue.labels && this.issue.labels.length;
},
labelDropdownTitle() {
- return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
- firstLabel: this.issue.labels[0].title,
- labelCount: this.issue.labels.length - 1
- }) : __('Label');
+ return this.hasLabels
+ ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
+ firstLabel: this.issue.labels[0].title,
+ labelCount: this.issue.labels.length - 1,
+ })
+ : __('Label');
},
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
- }
+ },
},
watch: {
detail: {
- handler () {
+ handler() {
if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
@@ -71,17 +73,19 @@ export default Vue.extend({
});
$('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('glDropdown').clearMenu();
+ $(el)
+ .data('glDropdown')
+ .clearMenu();
});
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
- deep: true
+ deep: true,
},
},
- created () {
+ created() {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
@@ -94,7 +98,7 @@ export default Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
- mounted () {
+ mounted() {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new DueDateSelectors();
@@ -102,29 +106,30 @@ export default Vue.extend({
new Sidebar();
},
methods: {
- closeSidebar () {
+ closeSidebar() {
this.detail.issue = {};
},
- assignSelf () {
+ assignSelf() {
// Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser);
this.saveAssignees();
},
- removeAssignee (a) {
+ removeAssignee(a) {
boardsStore.detail.issue.removeAssignee(a);
},
- addAssignee (a) {
+ addAssignee(a) {
boardsStore.detail.issue.addAssignee(a);
},
- removeAllAssignees () {
+ removeAllAssignees() {
boardsStore.detail.issue.removeAllAssignees();
},
- saveAssignees () {
+ saveAssignees() {
this.loadingAssignees = true;
- boardsStore.detail.issue.update()
+ boardsStore.detail.issue
+ .update()
.then(() => {
this.loadingAssignees = false;
})
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index aa98f35786e..d956777a86b 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,142 +1,142 @@
<script>
- import $ from 'jquery';
- import Icon from '~/vue_shared/components/icon.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 boardsStore from '../stores/boards_store';
+import $ from 'jquery';
+import Icon from '~/vue_shared/components/icon.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 boardsStore from '../stores/boards_store';
- export default {
- components: {
- UserAvatarLink,
- Icon,
- },
- directives: {
- tooltip,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- rootPath: {
- type: String,
- required: true,
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- groupId: {
- type: Number,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- limitBeforeCounter: 3,
- maxRender: 4,
- maxCounter: 99,
- };
+export default {
+ components: {
+ UserAvatarLink,
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ updateFilters: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ groupId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ limitBeforeCounter: 3,
+ maxRender: 4,
+ maxCounter: 99,
+ };
+ },
+ computed: {
+ numberOverLimit() {
+ return this.issue.assignees.length - this.limitBeforeCounter;
+ },
+ assigneeCounterTooltip() {
+ return `${this.assigneeCounterLabel} more`;
+ },
+ assigneeCounterLabel() {
+ if (this.numberOverLimit > this.maxCounter) {
+ return `${this.maxCounter}+`;
+ }
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
+ return `+${this.numberOverLimit}`;
+ },
+ shouldRenderCounter() {
+ if (this.issue.assignees.length <= this.maxRender) {
return false;
- },
- showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
+ }
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- return `${this.rootPath}${assignee.username}`;
- },
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
- avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- filterByLabel(label, e) {
- if (!this.updateFilters) return;
+ return this.issue.assignees.length > this.numberOverLimit;
+ },
+ issueId() {
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
+ },
+ showLabelFooter() {
+ return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ },
+ },
+ methods: {
+ isIndexLessThanlimit(index) {
+ return index < this.limitBeforeCounter;
+ },
+ shouldRenderAssignee(index) {
+ // Eg. maxRender is 4,
+ // Render up to all 4 assignees if there are only 4 assigness
+ // Otherwise render up to the limitBeforeCounter
+ if (this.issue.assignees.length <= this.maxRender) {
+ return index < this.maxRender;
+ }
+
+ return index < this.limitBeforeCounter;
+ },
+ assigneeUrl(assignee) {
+ return `${this.rootPath}${assignee.username}`;
+ },
+ assigneeUrlTitle(assignee) {
+ return `Assigned to ${assignee.name}`;
+ },
+ avatarUrlTitle(assignee) {
+ return `Avatar for ${assignee.name}`;
+ },
+ showLabel(label) {
+ if (!label.id) return false;
+ return true;
+ },
+ filterByLabel(label, e) {
+ if (!this.updateFilters) return;
- 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 filterPath = boardsStore.filter.path.split('&');
+ const labelTitle = encodeURIComponent(label.title);
+ const param = `label_name[]=${labelTitle}`;
+ const labelIndex = filterPath.indexOf(param);
+ $(e.currentTarget).tooltip('hide');
- if (labelIndex === -1) {
- filterPath.push(param);
- } else {
- filterPath.splice(labelIndex, 1);
- }
+ if (labelIndex === -1) {
+ filterPath.push(param);
+ } else {
+ filterPath.splice(labelIndex, 1);
+ }
- boardsStore.filter.path = filterPath.join('&');
+ boardsStore.filter.path = filterPath.join('&');
- boardsStore.updateFiltersUrl();
+ boardsStore.updateFiltersUrl();
- eventHub.$emit('updateTokens');
- },
- labelStyle(label) {
- return {
- backgroundColor: label.color,
- color: label.textColor,
- };
- },
- },
- };
+ eventHub.$emit('updateTokens');
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index dbd69f84526..795ba864545 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -20,7 +20,7 @@ export default {
computed: {
contents() {
const obj = {
- title: 'You haven\'t added any issues to your project yet',
+ title: "You haven't added any issues to your project yet",
content: `
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
@@ -28,7 +28,7 @@ export default {
};
if (this.activeTab === 'selected') {
- obj.title = 'You haven\'t selected any issues yet';
+ obj.title = "You haven't selected any issues yet";
obj.content = `
Go back to <strong>Open issues</strong> and select some issues
to add to your board.
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 268ca6bca13..d51597ed22d 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -42,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list);
// Post the data to the backend
- gl.boardService
- .bulkUpdate(issueIds, req)
- .catch(() => {
- Flash(__('Failed to update issues, please try again.'));
+ gl.boardService.bulkUpdate(issueIds, req).catch(() => {
+ Flash(__('Failed to update issues, please try again.'));
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
+ selectedIssues.forEach(issue => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
});
+ });
// Add the issues on the frontend
- selectedIssues.forEach((issue) => {
+ selectedIssues.forEach(issue => {
list.addIssue(issue);
list.issuesSize += 1;
});
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 979fb4d7199..fc6cefa89a9 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,52 +1,52 @@
<script>
- import ModalFilters from './filters';
- import ModalTabs from './tabs.vue';
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalFilters from './filters';
+import ModalTabs from './tabs.vue';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- components: {
- ModalTabs,
- ModalFilters,
+export default {
+ components: {
+ ModalTabs,
+ ModalFilters,
+ },
+ mixins: [modalMixin],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
},
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ milestonePath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ labelPath: {
+ type: String,
+ required: true,
},
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
- return 'Deselect all';
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
+ return 'Deselect all';
},
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.blur();
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
- ModalStore.toggleAll();
- },
+ ModalStore.toggleAll();
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 0c4c709324d..40949cc0656 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,143 +1,144 @@
<script>
- /* global ListIssue */
- import { urlParamsToObject } from '~/lib/utils/common_utils';
- import ModalHeader from './header.vue';
- import ModalList from './list.vue';
- import ModalFooter from './footer.vue';
- import EmptyState from './empty_state.vue';
- import ModalStore from '../../stores/modal_store';
+/* global ListIssue */
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import ModalHeader from './header.vue';
+import ModalList from './list.vue';
+import ModalFooter from './footer.vue';
+import EmptyState from './empty_state.vue';
+import ModalStore from '../../stores/modal_store';
- export default {
- components: {
- EmptyState,
- ModalHeader,
- ModalList,
- ModalFooter,
+export default {
+ components: {
+ EmptyState,
+ ModalHeader,
+ ModalList,
+ ModalFooter,
+ },
+ props: {
+ newIssuePath: {
+ type: String,
+ required: true,
},
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+ const loadingDone = () => {
+ this.loading = false;
+ };
+
+ this.loadIssues()
+ .then(loadingDone)
+ .catch(loadingDone);
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ if (this.$el.tagName) {
+ this.page = 1;
+ this.filterLoading = true;
const loadingDone = () => {
- this.loading = false;
+ this.filterLoading = false;
};
- this.loadIssues()
+ this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
}
},
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true)
- .then(loadingDone)
- .catch(loadingDone);
- }
- },
- deep: true,
- },
+ deep: true,
},
- created() {
- this.page = 1;
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
+ },
+ created() {
+ this.page = 1;
+ },
+ methods: {
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
- return gl.boardService.getBacklog({
+ return gl.boardService
+ .getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,
per: this.perPage,
})
- .then(res => res.data)
- .then(data => {
- if (clearIssues) {
- this.issues = [];
- }
+ .then(res => res.data)
+ .then(data => {
+ if (clearIssues) {
+ this.issues = [];
+ }
- data.issues.forEach(issueObj => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ data.issues.forEach(issueObj => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
- this.issues.push(issue);
- });
+ this.issues.push(issue);
+ });
- this.loadingNewPage = false;
+ this.loadingNewPage = false;
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- })
- .catch(() => {
- // TODO: handle request error
- });
- },
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ })
+ .catch(() => {
+ // TODO: handle request error
+ });
},
- };
+ },
+};
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index c93fd9f415c..e11f398e70d 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -1,120 +1,120 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import bp from '../../../breakpoints';
- import ModalStore from '../../stores/modal_store';
- import IssueCardInner from '../issue_card_inner.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import bp from '../../../breakpoints';
+import ModalStore from '../../stores/modal_store';
+import IssueCardInner from '../issue_card_inner.vue';
- export default {
- components: {
- IssueCardInner,
- Icon,
+export default {
+ components: {
+ IssueCardInner,
+ Icon,
+ },
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
},
- props: {
- issueLinkBase: {
- type: String,
- required: true,
- },
- rootPath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
+ rootPath: {
+ type: String,
+ required: true,
},
- data() {
- return ModalStore.store;
+ emptyStateSvg: {
+ type: String,
+ required: true,
},
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
- if (!groups[index]) {
- groups.push([]);
- }
+ if (!groups[index]) {
+ groups.push([]);
+ }
- groups[index].push(issue);
- });
+ groups[index].push(issue);
+ });
- return groups;
- },
+ return groups;
},
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
},
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if (
+ this.scrollTop() > this.scrollHeight() - 100 &&
+ !this.loadingNewPage &&
+ currentPage === this.page
+ ) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
},
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
},
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if (
- this.scrollTop() > this.scrollHeight() - 100 &&
- !this.loadingNewPage &&
- currentPage === this.page
- ) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
- const index = ModalStore.selectedIssueIndex(issue);
+ const index = ModalStore.selectedIssueIndex(issue);
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'lg' || breakpoint === 'md') {
- this.columns = 3;
- } else if (breakpoint === 'sm') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
},
- };
+ },
+};
</script>
<template>
<section
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
index 3baac08d411..20665f903d5 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
@@ -1,12 +1,12 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
Icon,
},
data() {
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index d926b080094..5d661590e8e 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,21 +1,21 @@
<script>
- import ModalStore from '../../stores/modal_store';
- import modalMixin from '../../mixins/modal_mixins';
+import ModalStore from '../../stores/modal_store';
+import modalMixin from '../../mixins/modal_mixins';
- export default {
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
+export default {
+ mixins: [modalMixin],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
},
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
- };
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+};
</script>
<template>
<div class="top-area prepend-top-10 append-bottom-10">
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2c2045f8901..f7016561f93 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -6,36 +6,41 @@ import _ from 'underscore';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
-$(document).off('created.label').on('created.label', (e, label) => {
- boardsStore.new({
- title: label.title,
- position: boardsStore.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
+$(document)
+ .off('created.label')
+ .on('created.label', (e, label) => {
+ boardsStore.new({
title: label.title,
- color: label.color,
- },
+ position: boardsStore.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color,
+ },
+ });
});
-});
export default function initNewListDropdown() {
- $('.js-new-board-list').each(function () {
+ $('.js-new-board-list').each(function() {
const $this = $(this);
- new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath'));
+ new CreateLabelDropdown(
+ $this.closest('.dropdown').find('.dropdown-new-label'),
+ $this.data('namespacePath'),
+ $this.data('projectPath'),
+ );
$this.glDropdown({
data(term, callback) {
- axios.get($this.attr('data-list-labels-path'))
- .then(({ data }) => {
- callback(data);
- });
+ axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
+ callback(data);
+ });
},
- renderRow (label) {
+ renderRow(label) {
const active = boardsStore.findList('title', label.title);
const $li = $('<li />');
const $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
+ class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title,
href: '#',
});
@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable: true,
multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
- clicked (options) {
+ clicked(options) {
const { e } = options;
const label = options.selectedObj;
e.preventDefault();
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 427a0868b0c..0f01a2a6c09 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -46,7 +46,7 @@ export default {
selectable: true,
data: (term, callback) => {
this.loading = true;
- return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => {
+ return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false;
callback(projects);
});
@@ -54,7 +54,9 @@ export default {
renderRow(project) {
return `
<li>
- <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
+ <a href='#' class='dropdown-menu-link' data-project-id="${
+ project.id
+ }" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index b8f2e324d43..d681e6a431c 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -1,79 +1,77 @@
<script>
- import Vue from 'vue';
- import Flash from '../../../flash';
- import { __ } from '../../../locale';
- import boardsStore from '../../stores/boards_store';
+import Vue from 'vue';
+import Flash from '../../../flash';
+import { __ } from '../../../locale';
+import boardsStore from '../../stores/boards_store';
- export default Vue.extend({
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
+export default Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
},
- computed: {
- updateUrl() {
- return this.issue.path;
- },
+ list: {
+ type: Object,
+ required: true,
},
- methods: {
- removeIssue() {
- const { issue } = this;
- const lists = issue.getLists();
- const req = this.buildPatchRequest(issue, lists);
-
- const data = {
- issue: this.seedPatchRequest(issue, req),
- };
+ },
+ computed: {
+ updateUrl() {
+ return this.issue.path;
+ },
+ },
+ methods: {
+ removeIssue() {
+ const { issue } = this;
+ const lists = issue.getLists();
+ const req = this.buildPatchRequest(issue, lists);
- if (data.issue.label_ids.length === 0) {
- data.issue.label_ids = [''];
- }
+ const data = {
+ issue: this.seedPatchRequest(issue, req),
+ };
- // Post the remove data
- Vue.http.patch(this.updateUrl, data).catch(() => {
- Flash(__('Failed to remove issue from board, please try again.'));
+ if (data.issue.label_ids.length === 0) {
+ data.issue.label_ids = [''];
+ }
- lists.forEach(list => {
- list.addIssue(issue);
- });
- });
+ // Post the remove data
+ Vue.http.patch(this.updateUrl, data).catch(() => {
+ Flash(__('Failed to remove issue from board, please try again.'));
- // Remove from the frontend store
lists.forEach(list => {
- list.removeIssue(issue);
+ list.addIssue(issue);
});
+ });
- boardsStore.detail.issue = {};
- },
- /**
- * Build the default patch request.
- */
- buildPatchRequest(issue, lists) {
- const listLabelIds = lists.map(list => list.label.id);
+ // Remove from the frontend store
+ lists.forEach(list => {
+ list.removeIssue(issue);
+ });
- const labelIds = issue.labels
- .map(label => label.id)
- .filter(id => !listLabelIds.includes(id));
+ boardsStore.detail.issue = {};
+ },
+ /**
+ * Build the default patch request.
+ */
+ buildPatchRequest(issue, lists) {
+ const listLabelIds = lists.map(list => list.label.id);
+
+ const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
- return {
- label_ids: labelIds,
- };
- },
- /**
- * Seed the given patch request.
- *
- * (This is overridden in EE)
- */
- seedPatchRequest(issue, req) {
- return req;
- },
+ return {
+ label_ids: labelIds,
+ };
+ },
+ /**
+ * Seed the given patch request.
+ *
+ * (This is overridden in EE)
+ */
+ seedPatchRequest(issue, req) {
+ return req;
},
- });
+ },
+});
</script>
<template>
<div
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index acf41e5689e..c14d69c5d18 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager
- [].forEach.call(tokens, (el) => {
+ [].forEach.call(tokens, el => {
el.parentNode.removeChild(el);
});
@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false;
- return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
- token.value === tokenValue) === -1;
+ return (
+ this.cantEditWithValue.findIndex(
+ token => token.name === tokenName && token.value === tokenValue,
+ ) === -1
+ );
}
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 91861f2f9ee..61a3072ac27 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -32,9 +32,9 @@ export default () => {
const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
- window.addEventListener('pageshow', (event) => {
- const isNavTypeBackForward = window.performance &&
- window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
+ window.addEventListener('pageshow', event => {
+ const isNavTypeBackForward =
+ window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index c9cde4effb9..983b28d2e67 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -4,7 +4,8 @@ import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config';
export function sortableStart() {
- $('.has-tooltip').tooltip('hide')
+ $('.has-tooltip')
+ .tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging');
}
@@ -15,7 +16,8 @@ export function sortableEnd() {
}
export function getBoardSortableDefaultOptions(obj) {
- const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+ const touchEnabled =
+ 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd: sortableEnd,
});
- Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ Object.keys(obj).forEach(key => {
+ defaultSortOptions[key] = obj[key];
+ });
return defaultSortOptions;
}
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 52d04389b88..669630edcab 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -9,7 +9,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store';
class ListIssue {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this.iid = obj.iid;
this.title = obj.title;
@@ -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.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) {
this.project = new IssueProject(obj.project);
@@ -39,54 +40,54 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone);
}
- obj.labels.forEach((label) => {
+ obj.labels.forEach(label => {
this.labels.push(new ListLabel(label));
});
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
- addLabel (label) {
+ addLabel(label) {
if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label));
}
}
- findLabel (findLabel) {
+ findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0];
}
- removeLabel (removeLabel) {
+ removeLabel(removeLabel) {
if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
- removeLabels (labels) {
+ removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this));
}
- addAssignee (assignee) {
+ addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee));
}
}
- findAssignee (findAssignee) {
+ findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
}
- removeAssignee (removeAssignee) {
+ removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
- removeAllAssignees () {
+ removeAllAssignees() {
this.assignees = [];
}
- getLists () {
+ getLists() {
return boardsStore.state.lists.filter(list => list.findIssue(this.id));
}
@@ -102,14 +103,14 @@ class ListIssue {
this.isLoading[key] = value;
}
- update () {
+ update() {
const data = {
issue: {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
- assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
- label_ids: this.labels.map((label) => label.id)
- }
+ assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
+ label_ids: this.labels.map(label => label.id),
+ },
};
if (!data.issue.label_ids.length) {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 3161f1da8c9..dd3feedbc0e 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -234,11 +234,11 @@ class List {
});
}
- getTypeInfo (type) {
+ getTypeInfo(type) {
return TYPES[type] || {};
}
- onNewIssueResponse (issue, data) {
+ onNewIssueResponse(issue, data) {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 029b0971f2c..3de6eb056c2 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,9 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
+ id ? `/${id}` : ''
+ }`;
}
all() {
@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) {
const data = { id };
- Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+ Object.keys(filter).forEach(key => {
+ data[key] = filter[key];
+ });
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
@@ -75,7 +79,9 @@ export default class BoardService {
}
getBacklog(data) {
- return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
+ return axios.get(
+ mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
+ );
}
bulkUpdate(issueIds, extraData = {}) {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 471955747fd..eefe14a1d79 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -20,20 +20,20 @@ const boardsStore = {
issue: {},
list: {},
},
- create () {
+ create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = {
issue: {},
};
},
- addList (listObj, defaultAvatar) {
+ addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
return list;
},
- new (listObj) {
+ new(listObj) {
const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog');
@@ -50,44 +50,44 @@ const boardsStore = {
});
this.removeBlankState();
},
- updateNewListDropdown (listId) {
+ updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active');
},
- shouldAddBlankState () {
+ shouldAddBlankState() {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]);
+ return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
},
- addBlankState () {
+ addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({
id: 'blank',
list_type: 'blank',
title: 'Welcome to your Issue Board!',
- position: 0
+ position: 0,
});
this.state.lists = _.sortBy(this.state.lists, 'position');
},
- removeBlankState () {
+ removeBlankState() {
this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10,
- path: ''
+ path: '',
});
},
- welcomeIsHidden () {
+ welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true';
},
- removeList (id, type = 'blank') {
+ removeList(id, type = 'blank') {
const list = this.findList('id', id, type);
if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id);
},
- moveList (listFrom, orderLists) {
+ moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10));
@@ -95,22 +95,25 @@ const boardsStore = {
});
listFrom.update();
},
- moveIssueToList (listFrom, listTo, issue, newIndex) {
+ moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
// Check if target list assignee is already present in this issue
- if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
- issue.findAssignee(listTo.assignee)) {
+ if (
+ listTo.type === 'assignee' &&
+ listFrom.type === 'assignee' &&
+ issue.findAssignee(listTo.assignee)
+ ) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone;
const currentLists = this.state.lists
- .filter(list => (list.type === 'milestone' && list.id !== listTo.id))
- .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
+ .filter(list => list.type === 'milestone' && list.id !== listTo.id)
+ .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone);
@@ -126,7 +129,7 @@ const boardsStore = {
}
if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
- issueLists.forEach((list) => {
+ issueLists.forEach(list => {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
@@ -144,26 +147,28 @@ const boardsStore = {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
- (listFrom.type === 'backlog')
+ listFrom.type === 'backlog'
);
},
- moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
- findList (key, val, type = 'label') {
- const filteredList = this.state.lists.filter((list) => {
- const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true;
+ findList(key, val, type = 'label') {
+ const filteredList = this.state.lists.filter(list => {
+ const byType = type
+ ? list.type === type || list.type === 'assignee' || list.type === 'milestone'
+ : true;
return list[key] === val && byType;
});
return filteredList[0];
},
- updateFiltersUrl () {
+ updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
- }
+ },
};
// hacks added in order to allow milestone_select to function properly
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 0d9ac367a70..b7228bf7bf5 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -40,7 +40,7 @@ class ModalStore {
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
- this.store.issues.forEach((issue) => {
+ this.store.issues.forEach(issue => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues
- .filter(fIssue => fIssue.id !== issue.id);
+ this.store.selectedIssues = this.store.selectedIssues.filter(
+ fIssue => fIssue.id !== issue.id,
+ );
}
}
purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
+ this.store.selectedIssues.forEach(issue => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
@@ -87,8 +88,7 @@ class ModalStore {
}
findSelectedIssue(issue) {
- return this.store.selectedIssues
- .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
}
}
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index 1089d0a72d3..c7a917457f3 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -47,7 +47,7 @@ export default class AjaxVariableList {
}
onSaveClicked() {
- const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon');
+ const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon');
loadingIcon.classList.toggle('hide', false);
this.errorBox.classList.toggle('hide', true);
// We use this to prevent a user from changing a key before we have a chance
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 6e7b5eb5526..6d7f45a35d8 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg';
+import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index a2aa3d197e3..82532539c9c 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -2,9 +2,15 @@
import PipelinesService from '../../pipelines/services/pipelines_service';
import PipelineStore from '../../pipelines/stores/pipelines_store';
import pipelinesMixin from '../../pipelines/mixins/pipelines';
+import TablePagination from '../../vue_shared/components/table_pagination.vue';
+import { getParameterByName } from '../../lib/utils/common_utils';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
- mixins: [pipelinesMixin],
+ components: {
+ TablePagination,
+ },
+ mixins: [pipelinesMixin, CIPaginationMixin],
props: {
endpoint: {
type: String,
@@ -35,6 +41,8 @@ export default {
return {
store,
state: store.state,
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
@@ -48,11 +56,14 @@ export default {
},
created() {
this.service = new PipelinesService(this.endpoint);
+ this.requestData = { page: this.page };
},
methods: {
successCallback(resp) {
// depending of the endpoint the response can either bring a `pipelines` key or not.
const pipelines = resp.data.pipelines || resp.data;
+
+ this.store.storePagination(resp.headers);
this.setCommonData(pipelines);
const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
@@ -97,5 +108,11 @@ export default {
:view-type="viewType"
/>
</div>
+
+ <table-pagination
+ v-if="shouldRenderPagination"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 1411f7ffd5e..82a191d056b 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -1,17 +1,6 @@
import Vue from 'vue';
-import {
- Pagination,
- ProgressBar,
- Modal,
- LoadingIcon,
- ModalDirective,
- TooltipDirective,
-} from '@gitlab-org/gitlab-ui';
+import { GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
-Vue.component('gl-pagination', Pagination);
-Vue.component('gl-progress-bar', ProgressBar);
-Vue.component('gl-ui-modal', Modal);
-Vue.component('gl-loading-icon', LoadingIcon);
+Vue.component('gl-loading-icon', GlLoadingIcon);
-Vue.directive('gl-modal', ModalDirective);
-Vue.directive('gl-tooltip', TooltipDirective);
+Vue.directive('gl-tooltip', GlTooltipDirective);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index a8d615dd8f0..59680959bb1 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -153,13 +153,9 @@ export default {
},
setDiscussions() {
if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
- requestIdleCallback(
- () =>
- this.assignDiscussionsToDiff().then(() => {
- this.assignedDiscussions = true;
- }),
- { timeout: 1000 },
- );
+ this.assignedDiscussions = true;
+
+ requestIdleCallback(() => this.assignDiscussionsToDiff(), { timeout: 1000 });
}
},
adjustView() {
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 9bbf62c0eb6..29b5aff0fb1 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -40,17 +40,14 @@ export default {
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
- isWhitespaceVisible() {
- return !getParameterValues('w')[0];
- },
toggleWhitespaceText() {
- if (this.isWhitespaceVisible) {
+ if (this.isWhitespaceVisible()) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
- if (this.isWhitespaceVisible) {
+ if (this.isWhitespaceVisible()) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
@@ -67,6 +64,9 @@ export default {
'expandAllFiles',
'toggleShowTreeList',
]),
+ isWhitespaceVisible() {
+ return getParameterValues('w')[0] !== '1';
+ },
},
};
</script>
@@ -121,7 +121,7 @@ export default {
</a>
<a
:href="toggleWhitespacePath"
- class="btn btn-default"
+ class="btn btn-default qa-toggle-whitespace"
>
{{ toggleWhitespaceText }}
</a>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 34e836a570a..91052b303a6 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
@@ -10,7 +10,7 @@ const treeListStorageKey = 'mr_diff_tree_list';
export default {
directives: {
- Tooltip,
+ GlTooltip: GlTooltipDirective,
},
components: {
Icon,
@@ -18,8 +18,8 @@ export default {
},
data() {
const treeListStored = localStorage.getItem(treeListStorageKey);
- const renderTreeList = treeListStored !== null ?
- convertPermissionToBoolean(treeListStored) : true;
+ const renderTreeList =
+ treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true;
return {
search: '',
@@ -101,7 +101,7 @@ export default {
class="btn-group prepend-left-8 tree-list-view-toggle"
>
<button
- v-tooltip.hover
+ v-gl-tooltip.hover
:aria-label="__('List view')"
:title="__('List view')"
:class="{
@@ -116,7 +116,7 @@ export default {
/>
</button>
<button
- v-tooltip.hover
+ v-gl-tooltip.hover
:aria-label="__('Tree view')"
:title="__('Tree view')"
:class="{
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 5a8aebd2086..38a65f111a2 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -133,7 +133,7 @@ export default {
},
right: {
...line.right,
- discussions: right ? line.right.discussions.concat(discussion) : [],
+ discussions: right && !left ? line.right.discussions.concat(discussion) : [],
},
};
}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index bb9c139727e..b62a5bb1940 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -15,7 +15,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
/**
- * Envrionment Item Component
+ * Environment Item Component
*
* Renders a table row for each environment.
*/
@@ -60,7 +60,7 @@ export default {
computed: {
/**
- * Verifies if `last_deployment` key exists in the current Envrionment.
+ * Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
* an helper.
*
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index a0797b594cb..26bec125445 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,14 +2,14 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
-import { Button } from '@gitlab-org/gitlab-ui';
+import { GlButton } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
- 'gl-button': Button,
+ GlButton,
},
directives: {
tooltip,
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index e2ecf426e64..557b2062c64 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,94 +1,92 @@
<script>
- import Flash from '../../flash';
- import { s__ } from '../../locale';
- import emptyState from './empty_state.vue';
- import eventHub from '../event_hub';
- import environmentsMixin from '../mixins/environments_mixin';
- import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
- import StopEnvironmentModal from './stop_environment_modal.vue';
+import Flash from '../../flash';
+import { s__ } from '../../locale';
+import emptyState from './empty_state.vue';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
+import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import StopEnvironmentModal from './stop_environment_modal.vue';
- export default {
- components: {
- emptyState,
- StopEnvironmentModal,
- },
+export default {
+ components: {
+ emptyState,
+ StopEnvironmentModal,
+ },
- mixins: [
- CIPaginationMixin,
- environmentsMixin,
- ],
+ mixins: [CIPaginationMixin, environmentsMixin],
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- canCreateEnvironment: {
- type: Boolean,
- required: true,
- },
- canCreateDeployment: {
- type: Boolean,
- required: true,
- },
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
- cssContainerClass: {
- type: String,
- required: true,
- },
- newEnvironmentPath: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
-
- created() {
- eventHub.$on('toggleFolder', this.toggleFolder);
+ canCreateEnvironment: {
+ type: Boolean,
+ required: true,
},
-
- beforeDestroy() {
- eventHub.$off('toggleFolder');
+ canCreateDeployment: {
+ type: Boolean,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cssContainerClass: {
+ type: String,
+ required: true,
+ },
+ newEnvironmentPath: {
+ type: String,
+ required: true,
},
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ created() {
+ eventHub.$on('toggleFolder', this.toggleFolder);
+ },
- methods: {
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
+ beforeDestroy() {
+ eventHub.$off('toggleFolder');
+ },
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
- },
+ methods: {
+ toggleFolder(folder) {
+ this.store.toggleFolder(folder);
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, true);
+ }
+ },
- this.service.getFolderContent(folder.folder_path)
- .then(response => this.store.setfolderContent(folder, response.data.environments))
- .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
- .catch(() => {
- Flash(s__('Environments|An error occurred while fetching the environments.'));
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
- },
+ fetchChildEnvironments(folder, showLoader = false) {
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
+
+ this.service
+ .getFolderContent(folder.folder_path)
+ .then(response => this.store.setfolderContent(folder, response.data.environments))
+ .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
+ .catch(() => {
+ Flash(s__('Environments|An error occurred while fetching the environments.'));
+ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
+ });
+ },
- successCallback(resp) {
- this.saveData(resp);
+ successCallback(resp) {
+ this.saveData(resp);
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach(folder => this.fetchChildEnvironments(folder));
- }
- },
+ // We need to verify if any folder is open to also update it
+ const openFolders = this.store.getOpenFolders();
+ if (openFolders.length) {
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
+ }
},
- };
+ },
+};
</script>
<template>
<div :class="cssContainerClass">
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5ce9225a4bb..5808a2d4afa 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -34,14 +34,14 @@ export default class EnvironmentsStore {
* @returns {Array}
*/
storeEnvironments(environments = []) {
- const filteredEnvironments = environments.map((env) => {
- const oldEnvironmentState = this.state.environments
- .find((element) => {
- if (env.latest) {
- return element.id === env.latest.id;
- }
- return element.id === env.id;
- }) || {};
+ const filteredEnvironments = environments.map(env => {
+ const oldEnvironmentState =
+ this.state.environments.find(element => {
+ if (env.latest) {
+ return element.id === env.latest.id;
+ }
+ return element.id === env.id;
+ }) || {};
let filtered = {};
@@ -101,11 +101,11 @@ export default class EnvironmentsStore {
}
/**
- * Toggles folder open property for the given folder.
- *
- * @param {Object} folder
- * @return {Array}
- */
+ * Toggles folder open property for the given folder.
+ *
+ * @param {Object} folder
+ * @return {Array}
+ */
toggleFolder(folder) {
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
}
@@ -119,7 +119,7 @@ export default class EnvironmentsStore {
* @return {Object}
*/
setfolderContent(folder, environments) {
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
let updated = env;
if (env.latest) {
@@ -148,7 +148,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp(environment, prop, newValue) {
const { environments } = this.state;
- const updatedEnvironments = environments.map((env) => {
+ const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env);
if (env.id === environment.id) {
updateEnv[prop] = newValue;
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index d36f38a70b5..d5027590bb7 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
}
itemClicked(e) {
- super.itemClicked(e,
- selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ super.itemClicked(e, selected =>
+ selected.querySelector('.dropdown-light-content').innerText.trim(),
+ );
}
renderContent(forceShowList = false) {
@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search
// with multiple words
- if (value[0] === '"' || value[0] === '\'') {
+ if (value[0] === '"' || value[0] === "'") {
value = value.slice(1);
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 4eb67ff7649..146d3ba963c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -85,7 +85,7 @@ export default class FilteredSearchDropdown {
}
dispatchInputEvent() {
- // Propogate input change to FilteredSearchDropdownManager
+ // Propagate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
this.input.dispatchEvent(
new CustomEvent('input', {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cd3d532c958..57ec6603d80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager {
},
};
- supportedTokens.forEach((type) => {
+ supportedTokens.forEach(type => {
if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type];
}
@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager {
}
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
- const {
- uppercaseTokenName = false,
- capitalizeTokenValue = false,
- } = options;
+ const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName,
@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) {
// Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
+ let offset =
+ this.filteredSearchInput.getBoundingClientRect().left -
+ this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container
- const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ const offsetMaxWidth =
+ this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)`
- mappingKey.reference =
- new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
+ mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
}
if (firstLoad) {
@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager {
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
- const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
- && this.mapping[match.key];
+ const shouldOpenFilterDropdown =
+ match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() {
const query = DropdownUtils.getSearchQuery(true);
- const { lastToken, searchToken } =
- this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys());
+ const { lastToken, searchToken } = this.tokenizer.processTokens(
+ query,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 54533ebb70d..4a2af02b40a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,8 +1,5 @@
import _ from 'underscore';
-import {
- getParameterByName,
- getUrlParamsArray,
-} from '~/lib/utils/common_utils';
+import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
@@ -48,24 +45,28 @@ export default class FilteredSearchManager {
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
allowedKeys: this.filteredSearchTokenKeys.getKeys(),
});
- this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
- const fullPath = this.searchHistoryDropdownElement ?
- this.searchHistoryDropdownElement.dataset.fullPath : 'project';
+ this.searchHistoryDropdownElement = document.querySelector(
+ '.js-filtered-search-history-dropdown',
+ );
+ const fullPath = this.searchHistoryDropdownElement
+ ? this.searchHistoryDropdownElement.dataset.fullPath
+ : 'project';
const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
setup() {
// Fetch recent searches from localStorage
- this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch()
- .catch((error) => {
+ this.fetchingRecentSearchesPromise = this.recentSearchesService
+ .fetch()
+ .catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
- .then((searches) => {
+ .then(searches => {
if (!searches) {
return;
}
@@ -120,7 +121,7 @@ export default class FilteredSearchManager {
if (this.stateFilters) {
this.searchStateWrapper = this.searchState.bind(this);
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.addEventListener('click', this.searchStateWrapper);
});
}
@@ -128,14 +129,14 @@ export default class FilteredSearchManager {
unbindStateEvents() {
if (this.stateFilters) {
- this.applyToStateFilters((filterEl) => {
+ this.applyToStateFilters(filterEl => {
filterEl.removeEventListener('click', this.searchStateWrapper);
});
}
}
applyToStateFilters(callback) {
- this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => {
+ this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => {
if (this.states.indexOf(filterEl.dataset.state) > -1) {
callback(filterEl);
}
@@ -207,7 +208,7 @@ export default class FilteredSearchManager {
let backspaceCount = 0;
// closure for keeping track of the number of backspace keystrokes
- return (e) => {
+ return e => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
@@ -274,8 +275,12 @@ export default class FilteredSearchManager {
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
- if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
- !isElementInStaticFilterDropdown && inputContainer) {
+ if (
+ !isElementInFilteredSearch &&
+ !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown &&
+ inputContainer
+ ) {
inputContainer.classList.remove('focus');
}
}
@@ -368,7 +373,7 @@ export default class FilteredSearchManager {
const removeElements = [];
- [].forEach.call(this.tokensContainer.children, (t) => {
+ [].forEach.call(this.tokensContainer.children, t => {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
@@ -381,7 +386,7 @@ export default class FilteredSearchManager {
}
});
- removeElements.forEach((el) => {
+ removeElements.forEach(el => {
el.parentElement.removeChild(el);
});
@@ -397,13 +402,14 @@ export default class FilteredSearchManager {
handleInputVisualToken() {
const input = this.filteredSearchInput;
- const { tokens, searchToken }
- = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys());
- const { isLastVisualTokenValid }
- = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const { tokens, searchToken } = this.tokenizer.processTokens(
+ input.value,
+ this.filteredSearchTokenKeys.getKeys(),
+ );
+ const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
- tokens.forEach((t) => {
+ tokens.forEach(t => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
@@ -453,15 +459,17 @@ export default class FilteredSearchManager {
saveCurrentSearchQuery() {
// Don't save before we have fetched the already saved searches
- this.fetchingRecentSearchesPromise.then(() => {
- const searchQuery = DropdownUtils.getSearchQuery();
- if (searchQuery.length > 0) {
- const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
- this.recentSearchesService.save(resultantSearches);
- }
- }).catch(() => {
- // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
- });
+ this.fetchingRecentSearchesPromise
+ .then(() => {
+ const searchQuery = DropdownUtils.getSearchQuery();
+ if (searchQuery.length > 0) {
+ const resultantSearches = this.recentSearchesStore.addRecentSearch(searchQuery);
+ this.recentSearchesService.save(resultantSearches);
+ }
+ })
+ .catch(() => {
+ // https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
+ });
}
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
@@ -475,7 +483,7 @@ export default class FilteredSearchManager {
const usernameParams = this.getUsernameParams();
let hasFilteredSearch = false;
- params.forEach((p) => {
+ params.forEach(p => {
const split = p.split('=');
const keyParam = decodeURIComponent(split[0]);
const value = split[1];
@@ -486,11 +494,9 @@ export default class FilteredSearchManager {
if (condition) {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(condition.tokenKey);
- FilteredSearchVisualTokens.addFilterVisualToken(
- condition.tokenKey,
- condition.value,
- { canEdit },
- );
+ FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value, {
+ canEdit,
+ });
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@@ -510,7 +516,7 @@ export default class FilteredSearchManager {
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
- quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : "'";
}
hasFilteredSearch = true;
@@ -531,7 +537,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
@@ -539,7 +547,9 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
- FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
+ FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, {
+ canEdit,
+ });
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
@@ -580,9 +590,11 @@ export default class FilteredSearchManager {
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
- tokens.forEach((token) => {
- const condition = this.filteredSearchTokenKeys
- .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ tokens.forEach(token => {
+ const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
+ token.key,
+ token.value.toLowerCase(),
+ );
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
@@ -601,8 +613,10 @@ export default class FilteredSearchManager {
tokenValue = tokenValue.toLowerCase();
}
- if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
- (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ if (
+ (tokenValue[0] === "'" && tokenValue[tokenValue.length - 1] === "'") ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')
+ ) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
@@ -613,7 +627,10 @@ export default class FilteredSearchManager {
});
if (searchToken) {
- const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+');
+ const sanitized = searchToken
+ .split(' ')
+ .map(t => encodeURIComponent(t))
+ .join('+');
paths.push(`search=${sanitized}`);
}
@@ -630,7 +647,7 @@ export default class FilteredSearchManager {
const usernamesById = {};
try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
- JSON.parse(attribute).forEach((user) => {
+ JSON.parse(attribute).forEach(user => {
usernamesById[user.id] = user.username;
});
} catch (e) {
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index b70125c80ca..bb0ecb8efe7 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -58,17 +58,22 @@ export const alternativeTokenKeys = [
export const conditions = [
{
- url: 'assignee_id=0',
+ url: 'assignee_id=None',
tokenKey: 'assignee',
value: 'none',
},
{
- url: 'milestone_title=No+Milestone',
+ url: 'assignee_id=Any',
+ tokenKey: 'assignee',
+ value: 'any',
+ },
+ {
+ url: 'milestone_title=None',
tokenKey: 'milestone',
value: 'none',
},
{
- url: 'milestone_title=Any+Milestone',
+ url: 'milestone_title=Any',
tokenKey: 'milestone',
value: 'any',
},
@@ -87,6 +92,16 @@ export const conditions = [
tokenKey: 'label',
value: 'none',
},
+ {
+ url: 'my_reaction_emoji=None',
+ tokenKey: 'my-reaction',
+ value: 'none',
+ },
+ {
+ url: 'my_reaction_emoji=Any',
+ tokenKey: 'my-reaction',
+ value: 'any',
+ },
];
const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 749c09f897c..c2397842125 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-${type}"
>
<div
- class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
+ class="flash-text ${
+ isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
+ }"
>
${_.escape(message)}
</div>
@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if (!flashContainer) return null;
- const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true;
+ const isFixedLayout = navigation
+ ? navigation.parentNode.classList.contains('container-limited')
+ : true;
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 95636a9ccdd..00b3d283570 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -94,7 +94,7 @@ class GfmAutoComplete {
...this.getDefaultCallbacks(),
beforeSave(commands) {
if (GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, (c) => {
+ return $.map(commands, c => {
let search = c.name;
if (c.aliases.length > 0) {
search = `${search} ${c.aliases.join(' ')}`;
@@ -167,7 +167,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(members) {
- return $.map(members, (m) => {
+ return $.map(members, m => {
let title = '';
if (m.username == null) {
return m;
@@ -178,7 +178,9 @@ class GfmAutoComplete {
}
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
- const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${
+ m.username
+ }" class="avatar avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
return {
@@ -201,7 +203,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -211,7 +213,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
- return $.map(issues, (i) => {
+ return $.map(issues, i => {
if (i.title == null) {
return i;
}
@@ -244,7 +246,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(milestones) {
- return $.map(milestones, (m) => {
+ return $.map(milestones, m => {
if (m.title == null) {
return m;
}
@@ -267,7 +269,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -277,7 +279,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
- return $.map(merges, (m) => {
+ return $.map(merges, m => {
if (m.title == null) {
return m;
}
@@ -324,13 +326,20 @@ class GfmAutoComplete {
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
- const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
+ const subtextNodes = subtext
+ .split(/\n+/g)
+ .pop()
+ .split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
- command = subtextNodes.find((node) => {
- if (node === LABEL_COMMAND.LABEL ||
- node === LABEL_COMMAND.RELABEL ||
- node === LABEL_COMMAND.UNLABEL) { return node; }
+ command = subtextNodes.find(node => {
+ if (
+ node === LABEL_COMMAND.LABEL ||
+ node === LABEL_COMMAND.RELABEL ||
+ node === LABEL_COMMAND.UNLABEL
+ ) {
+ return node;
+ }
return null;
});
@@ -370,7 +379,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.template;
+ tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@@ -380,7 +389,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(snippets) {
- return $.map(snippets, (m) => {
+ return $.map(snippets, m => {
if (m.title == null) {
return m;
}
@@ -458,13 +467,17 @@ class GfmAutoComplete {
this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
- .then((data) => {
+ .then(data => {
this.loadData($input, at, data);
})
- .catch(() => { this.isLoadingData[at] = false; });
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
} else {
this.isLoadingData[at] = false;
}
@@ -497,15 +510,16 @@ class GfmAutoComplete {
}
const loadingState = GfmAutoComplete.defaultLoadingData[0];
- return dataToInspect &&
- (dataToInspect === loadingState || dataToInspect.name === loadingState);
+ return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
}
static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
- const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&');
+ const atSymbolsWithBar = Object.keys(controllers)
+ .join('|')
+ .replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
@@ -513,7 +527,10 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
- const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+ const regexp = new RegExp(
+ `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
+ 'gi',
+ );
return regexp.exec(targetSubtext);
}
@@ -552,13 +569,15 @@ GfmAutoComplete.Members = {
template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
};
GfmAutoComplete.Labels = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+ template:
+ // eslint-disable-next-line no-template-curly-in-string
+ '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li><small>${id}</small> ${title}</li>',
+ templateFunction(id, title) {
+ return `<li><small>${id}</small> ${_.escape(title)}</li>`;
+ },
};
// Milestones
GfmAutoComplete.Milestones = {
@@ -566,7 +585,8 @@ GfmAutoComplete.Milestones = {
template: '<li>${title}</li>',
};
GfmAutoComplete.Loading = {
- template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
+ template:
+ '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
};
export default GfmAutoComplete;
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 3764e7ab422..d5d5954ce6a 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -28,7 +28,7 @@ export default class GlFieldErrors {
this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
- /* Neccessary to prevent intercept and override invalid form submit
+ /* Necessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 4365305c168..903c838e266 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { slugifyWithHyphens } from './lib/utils/text_utility';
export default class Group {
constructor() {
@@ -7,17 +8,18 @@ export default class Group {
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
if (this.groupName.val() === '') {
- this.groupPath.on('keyup', this.updateHandler);
- this.groupName.on('keydown', this.resetHandler);
+ this.groupName.on('keyup', this.updateHandler);
+ this.groupPath.on('keydown', this.resetHandler);
}
}
update() {
- this.groupName.val(this.groupPath.val());
+ const slug = slugifyWithHyphens(this.groupName.val());
+ this.groupPath.val(slug);
}
reset() {
- this.groupPath.off('keyup', this.updateHandler);
- this.groupName.off('keydown', this.resetHandler);
+ this.groupName.off('keyup', this.updateHandler);
+ this.groupPath.off('keydown', this.resetHandler);
}
}
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index ad6151e3bf6..0a368f6558c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -43,7 +43,7 @@ export default {
'currentProjectId',
'errorMessage',
]),
- ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
+ ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
@@ -63,7 +63,7 @@ export default {
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
- if (!this.someUncommitedChanges) return undefined;
+ if (!this.someUncommittedChanges) return undefined;
Object.assign(e, {
returnValue,
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index dc84ee12f1e..364ab9426e0 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapGetters } from 'vuex';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
@@ -13,7 +13,7 @@ import { activityBarViews } from '../constants';
export default {
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
ResizablePanel,
ActivityBar,
CommitSection,
@@ -25,11 +25,11 @@ export default {
},
computed: {
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
- ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ ...mapGetters(['currentProject', 'someUncommittedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
- (this.lastCommitMsg && !this.someUncommitedChanges)
+ (this.lastCommitMsg && !this.someUncommittedChanges)
);
},
},
@@ -50,7 +50,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e88f01fb4f4..d2ff55a4ee3 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
@@ -9,7 +9,7 @@ import FileRowExtra from './file_row_extra.vue';
export default {
components: {
Icon,
- SkeletonLoading,
+ GlSkeletonLoading,
NavDropdown,
FileRow,
},
@@ -51,7 +51,7 @@ export default {
:key="n"
class="multi-file-loading-container"
>
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</template>
<template v-else>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 0a2681b7a1e..b670b0355b7 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -25,7 +25,7 @@ export default {
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
ciLintText() {
return sprintf(
- __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
+ __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
{
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d3b24c5b793..5e86876c1c1 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -27,10 +27,10 @@ export default {
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']),
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 709748fb530..8ad85074d6b 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
-export const someUncommitedChanges = state =>
+export const someUncommittedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 17fd5321642..93c89411b4a 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -1,10 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
mixins: [timeagoMixin],
props: {
@@ -53,16 +55,16 @@ export default {
class="btn-group d-flex"
role="group"
>
- <a
+ <gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
>
{{ s__('Job|Keep') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.download_path"
:href="artifact.download_path"
class="js-download-artifacts btn btn-sm btn-default"
@@ -70,15 +72,15 @@ export default {
rel="nofollow"
>
{{ s__('Job|Download') }}
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
>
{{ s__('Job|Browse') }}
- </a>
+ </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 7d51f6afd10..06fe23fedce 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -1,9 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
+ GlLink,
},
props: {
commit: {
@@ -31,10 +33,10 @@ export default {
<p>
{{ __('Commit') }}
- <a
+ <gl-link
:href="commit.commit_path"
class="js-commit-sha commit-sha link-commit"
- >{{ commit.short_id }}</a>
+ >{{ commit.short_id }}</gl-link>
<clipboard-button
:text="commit.short_id"
@@ -42,11 +44,11 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <a
+ <gl-link
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</a>
+ >!{{ mergeRequest.iid }}</gl-link>
</p>
<p class="build-light-text append-bottom-0">
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index ee5ceb99b0a..be7425c2d25 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -1,5 +1,10 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
+ components: {
+ GlLink,
+ },
props: {
illustrationPath: {
type: String,
@@ -62,13 +67,13 @@ export default {
v-if="action"
class="text-center"
>
- <a
+ <gl-link
:href="action.path"
:data-method="action.method"
class="js-job-empty-state-action btn btn-primary"
>
{{ action.button_title }}
- </a>
+ </gl-link>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue
index 5ffbfb6e19a..d80e905c68e 100644
--- a/app/assets/javascripts/jobs/components/erased_block.vue
+++ b/app/assets/javascripts/jobs/components/erased_block.vue
@@ -1,10 +1,12 @@
<script>
import _ from 'underscore';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
+ GlLink,
},
props: {
user: {
@@ -29,9 +31,9 @@ export default {
<div class="erased alert alert-warning">
<template v-if="isErasedByUser">
{{ s__("Job|Job has been erased by") }}
- <a :href="user.web_url">
+ <gl-link :href="user.web_url">
{{ user.username }}
- </a>
+ </gl-link>
</template>
<template v-else>
{{ s__("Job|Job has been erased") }}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index ac19034f69d..6e95e3d16f8 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -1,164 +1,166 @@
<script>
- import _ from 'underscore';
- import { mapGetters, mapState, mapActions } from 'vuex';
- import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
- import bp from '~/breakpoints';
- import CiHeader from '~/vue_shared/components/header_ci_component.vue';
- import Callout from '~/vue_shared/components/callout.vue';
- import createStore from '../store';
- import EmptyState from './empty_state.vue';
- import EnvironmentsBlock from './environments_block.vue';
- import ErasedBlock from './erased_block.vue';
- 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 _ from 'underscore';
+import { mapGetters, mapState, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+import bp from '~/breakpoints';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import Callout from '~/vue_shared/components/callout.vue';
+import createStore from '../store';
+import EmptyState from './empty_state.vue';
+import EnvironmentsBlock from './environments_block.vue';
+import ErasedBlock from './erased_block.vue';
+import Log from './job_log.vue';
+import LogTopBar from './job_log_controllers.vue';
+import StuckBlock from './stuck_block.vue';
+import Sidebar from './sidebar.vue';
- export default {
- name: 'JobPageApp',
- store: createStore(),
- components: {
- CiHeader,
- Callout,
- EmptyState,
- EnvironmentsBlock,
- ErasedBlock,
- Log,
- LogTopBar,
- StuckBlock,
- Sidebar,
+export default {
+ name: 'JobPageApp',
+ store: createStore(),
+ components: {
+ CiHeader,
+ Callout,
+ EmptyState,
+ EnvironmentsBlock,
+ ErasedBlock,
+ GlLoadingIcon,
+ Log,
+ LogTopBar,
+ StuckBlock,
+ Sidebar,
+ },
+ props: {
+ runnerSettingsUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- props: {
- runnerSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: null,
- },
- endpoint: {
- type: String,
- required: true,
- },
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- pagePath: {
- type: String,
- required: true,
- },
- logState: {
- type: String,
- required: true,
- },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState([
- 'isLoading',
- 'job',
- 'isSidebarOpen',
- 'trace',
- 'isTraceComplete',
- 'traceSize',
- 'isTraceSizeVisible',
- 'isScrollBottomDisabled',
- 'isScrollTopDisabled',
- 'isScrolledToBottomBeforeReceivingTrace',
- 'hasError',
- ]),
- ...mapGetters([
- 'headerActions',
- 'headerTime',
- 'shouldRenderCalloutMessage',
- 'shouldRenderTriggeredLabel',
- 'hasEnvironment',
- 'hasTrace',
- 'emptyStateIllustration',
- 'isScrollingDown',
- 'emptyStateAction',
- 'hasRunnersForProject',
- ]),
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ pagePath: {
+ type: String,
+ required: true,
+ },
+ logState: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'isLoading',
+ 'job',
+ 'isSidebarOpen',
+ 'trace',
+ 'isTraceComplete',
+ 'traceSize',
+ 'isTraceSizeVisible',
+ 'isScrollBottomDisabled',
+ 'isScrollTopDisabled',
+ 'isScrolledToBottomBeforeReceivingTrace',
+ 'hasError',
+ ]),
+ ...mapGetters([
+ 'headerActions',
+ 'headerTime',
+ 'shouldRenderCalloutMessage',
+ 'shouldRenderTriggeredLabel',
+ 'hasEnvironment',
+ 'hasTrace',
+ 'emptyStateIllustration',
+ 'isScrollingDown',
+ 'emptyStateAction',
+ 'hasRunnersForProject',
+ ]),
- shouldRenderContent() {
- return !this.isLoading && !this.hasError;
- }
+ shouldRenderContent() {
+ return !this.isLoading && !this.hasError;
},
- watch: {
- // Once the job log is loaded,
- // fetch the stages for the dropdown on the sidebar
- job(newVal, oldVal) {
- if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
- this.fetchStages();
- }
- },
+ },
+ watch: {
+ // Once the job log is loaded,
+ // fetch the stages for the dropdown on the sidebar
+ job(newVal, oldVal) {
+ if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
+ this.fetchStages();
+ }
},
- created() {
- this.throttled = _.throttle(this.toggleScrollButtons, 100);
+ },
+ created() {
+ this.throttled = _.throttle(this.toggleScrollButtons, 100);
- this.setJobEndpoint(this.endpoint);
- this.setTraceOptions({
- logState: this.logState,
- pagePath: this.pagePath,
- });
+ this.setJobEndpoint(this.endpoint);
+ this.setTraceOptions({
+ logState: this.logState,
+ pagePath: this.pagePath,
+ });
- this.fetchJob();
- this.fetchTrace();
+ this.fetchJob();
+ this.fetchTrace();
- window.addEventListener('resize', this.onResize);
- window.addEventListener('scroll', this.updateScroll);
- },
+ window.addEventListener('resize', this.onResize);
+ window.addEventListener('scroll', this.updateScroll);
+ },
- mounted() {
+ mounted() {
+ this.updateSidebar();
+ },
+
+ destroyed() {
+ window.removeEventListener('resize', this.onResize);
+ window.removeEventListener('scroll', this.updateScroll);
+ },
+
+ methods: {
+ ...mapActions([
+ 'setJobEndpoint',
+ 'setTraceOptions',
+ 'fetchJob',
+ 'fetchStages',
+ 'hideSidebar',
+ 'showSidebar',
+ 'toggleSidebar',
+ 'fetchTrace',
+ 'scrollBottom',
+ 'scrollTop',
+ 'toggleScrollButtons',
+ 'toggleScrollAnimation',
+ ]),
+ onResize() {
this.updateSidebar();
+ this.updateScroll();
},
-
- destroyed() {
- window.removeEventListener('resize', this.onResize);
- window.removeEventListener('scroll', this.updateScroll);
+ updateSidebar() {
+ if (bp.getBreakpointSize() === 'xs') {
+ this.hideSidebar();
+ } else if (!this.isSidebarOpen) {
+ this.showSidebar();
+ }
},
+ updateScroll() {
+ if (!isScrolledToBottom()) {
+ this.toggleScrollAnimation(false);
+ } else if (this.isScrollingDown) {
+ this.toggleScrollAnimation(true);
+ }
- methods: {
- ...mapActions([
- 'setJobEndpoint',
- 'setTraceOptions',
- 'fetchJob',
- 'fetchStages',
- 'hideSidebar',
- 'showSidebar',
- 'toggleSidebar',
- 'fetchTrace',
- 'scrollBottom',
- 'scrollTop',
- 'toggleScrollButtons',
- 'toggleScrollAnimation',
- ]),
- onResize() {
- this.updateSidebar();
- this.updateScroll();
- },
- updateSidebar() {
- if (bp.getBreakpointSize() === 'xs') {
- this.hideSidebar();
- } else if (!this.isSidebarOpen) {
- this.showSidebar();
- }
- },
- updateScroll() {
- if (!isScrolledToBottom()) {
- this.toggleScrollAnimation(false);
- } else if (this.isScrollingDown) {
- this.toggleScrollAnimation(true);
- }
-
- this.throttled();
- },
+ this.throttled();
},
- };
+ },
+};
</script>
<template>
<div>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 6486b25c8a7..cdac8a391d1 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,15 +1,16 @@
<script>
+import { GlTooltipDirective, GlLink } from '@gitlab-org/gitlab-ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
CiIcon,
Icon,
+ GlLink,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
job: {
@@ -37,11 +38,10 @@ export default {
active: isActive
}"
>
- <a
- v-tooltip
+ <gl-link
+ v-gl-tooltip
:href="job.status.details_path"
:title="tooltipText"
- data-container="body"
data-boundary="viewport"
class="js-job-link"
>
@@ -60,6 +60,6 @@ export default {
name="retry"
class="js-retry-icon"
/>
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue
index ffa6ada3e28..92e20e92d66 100644
--- a/app/assets/javascripts/jobs/components/job_log.vue
+++ b/app/assets/javascripts/jobs/components/job_log.vue
@@ -1,45 +1,45 @@
<script>
- import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions } from 'vuex';
- export default {
- name: 'JobLog',
- props: {
- trace: {
- type: String,
- required: true,
- },
- isComplete: {
- type: Boolean,
- required: true,
- },
+export default {
+ name: 'JobLog',
+ props: {
+ trace: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ isComplete: {
+ type: Boolean,
+ required: true,
},
- updated() {
- this.$nextTick(() => this.handleScrollDown());
+ },
+ computed: {
+ ...mapState(['isScrolledToBottomBeforeReceivingTrace']),
+ },
+ updated() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ mounted() {
+ this.$nextTick(() => this.handleScrollDown());
+ },
+ methods: {
+ ...mapActions(['scrollBottom']),
+ /**
+ * The job log is sent in HTML, which means we need to use `v-html` to render it
+ * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
+ * in this case because it runs before `v-html` has finished running, since there's no
+ * Vue binding.
+ * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
+ */
+ handleScrollDown() {
+ if (this.isScrolledToBottomBeforeReceivingTrace) {
+ setTimeout(() => {
+ this.scrollBottom();
+ }, 0);
+ }
},
- mounted() {
- this.$nextTick(() => this.handleScrollDown());
- },
- methods: {
- ...mapActions(['scrollBottom']),
- /**
- * The job log is sent in HTML, which means we need to use `v-html` to render it
- * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
- * in this case because it runs before `v-html` has finished running, since there's no
- * Vue binding.
- * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
- */
- handleScrollDown() {
- if (this.isScrolledToBottomBeforeReceivingTrace) {
- setTimeout(() => {
- this.scrollBottom();
- }, 0);
- }
- },
- },
- };
+ },
+};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 94ab1b16c84..eeefa33264f 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -1,7 +1,7 @@
<script>
+import { GlTooltipDirective, GlLink, GlButton } from '@gitlab-org/gitlab-ui';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import scrollDown from '../svg/scroll_down.svg';
@@ -9,9 +9,11 @@ import scrollDown from '../svg/scroll_down.svg';
export default {
components: {
Icon,
+ GlLink,
+ GlButton,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
scrollDown,
props: {
@@ -73,76 +75,70 @@ export default {
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
- <a
+ <gl-link
v-if="rawPath"
:href="rawPath"
class="js-raw-link raw-link"
>
{{ s__("Job|Complete Raw") }}
- </a>
+ </gl-link>
</template>
</div>
<!-- eo truncate information -->
<div class="controllers float-right">
<!-- links -->
- <a
+ <gl-link
v-if="rawPath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
class="js-raw-link-controller controllers-buttons"
- data-container="body"
>
<icon name="doc-text" />
- </a>
+ </gl-link>
- <a
+ <gl-link
v-if="erasePath"
- v-tooltip
+ v-gl-tooltip.body
:title="s__('Job|Erase job log')"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
class="js-erase-link controllers-buttons"
- data-container="body"
data-method="post"
>
<icon name="remove" />
- </a>
+ </gl-link>
<!-- eo links -->
<!-- scroll buttons -->
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to top')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollTopDisabled"
type="button"
class="js-scroll-top btn-scroll btn-transparent btn-blank"
@click="handleScrollToTop"
>
- <icon name="scroll_up"/>
- </button>
+ <icon name="scroll_up" />
+ </gl-button>
</div>
<div
- v-tooltip
+ v-gl-tooltip
:title="s__('Job|Scroll to bottom')"
class="controllers-buttons"
- data-container="body"
>
- <button
+ <gl-button
:disabled="isScrollBottomDisabled"
- type="button"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
- >
- </button>
+ />
</div>
<!-- eo scroll buttons -->
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index aeafe98a70b..cfedb38e17a 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -1,6 +1,11 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
+
export default {
name: 'SidebarDetailRow',
+ components: {
+ GlLink,
+ },
props: {
title: {
type: String,
@@ -41,7 +46,7 @@ export default {
v-if="hasHelpURL"
class="help-button float-right"
>
- <a
+ <gl-link
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -50,7 +55,7 @@ export default {
class="fa fa-question-circle"
aria-hidden="true"
></i>
- </a>
+ </gl-link>
</span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index 1d5789b175a..ca4bf471363 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,8 +1,12 @@
<script>
+import { GlLink } from '@gitlab-org/gitlab-ui';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
+ components: {
+ GlLink,
+ },
props: {
hasNoRunnersForProject: {
type: Boolean,
@@ -52,12 +56,12 @@ export default {
</p>
{{ __("Go to") }}
- <a
+ <gl-link
v-if="runnersPath"
:href="runnersPath"
class="js-runners-path"
>
{{ __("Runners page") }}
- </a>
+ </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index ccd096a1da5..a32e945627c 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -23,4 +23,3 @@ export default () => {
},
});
};
-
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 4de01f8e532..d440b2c9ef1 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
-export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
+export const hasTrace = state =>
+ state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
-export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
+export const emptyStateAction = state =>
+ (state.job && state.job.status && state.job.status.action) || {};
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
-export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online;
+export const hasRunnersForProject = state =>
+ state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 3c38d998b6c..c0a76814102 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -25,16 +25,43 @@ export default class LabelsSelect {
}
$els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ var $block,
+ $colorPreview,
+ $dropdown,
+ $form,
+ $loading,
+ $selectbox,
+ $sidebarCollapsedValue,
+ $value,
+ abilityName,
+ defaultLabel,
+ enableLabelCreateButton,
+ issueURLSplit,
+ issueUpdateURL,
+ labelUrl,
+ namespacePath,
+ projectPath,
+ saveLabelData,
+ selectedLabel,
+ showAny,
+ showNo,
+ $sidebarLabelTooltip,
+ initialSelected,
+ $toggleText,
+ fieldName,
+ useId,
+ propertyName,
+ showMenuAbove,
+ $container,
+ $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath');
- labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
- if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
showNo = $dropdown.data('showNo');
@@ -50,26 +77,37 @@ export default class LabelsSelect {
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName');
- useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ useId = $dropdown.is(
+ '.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
+ );
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]')
- .map(function () {
+ .map(function() {
return this.value;
- }).get();
+ })
+ .get();
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
+ new CreateLabelDropdown(
+ $dropdown.closest('.dropdown').find('.dropdown-new-label'),
+ namespacePath,
+ projectPath,
+ );
}
saveLabelData = function() {
var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
- return this.value;
- }).get();
+ selected = $dropdown
+ .closest('.selectbox')
+ .find("input[name='" + fieldName + "']")
+ .map(function() {
+ return this.value;
+ })
+ .get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
@@ -82,7 +120,8 @@ export default class LabelsSelect {
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- axios.put(issueUpdateURL, data)
+ axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
@@ -96,8 +135,7 @@ export default class LabelsSelect {
issueUpdateURL,
});
labelCount = data.labels.length;
- }
- else {
+ } else {
template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
@@ -114,17 +152,14 @@ export default class LabelsSelect {
}
labelTooltipTitle = labelTitles.join(', ');
- }
- else {
+ } else {
labelTooltipTitle = __('Labels');
}
- $sidebarLabelTooltip
- .attr('title', labelTooltipTitle)
- .tooltip('_fixTitle');
+ $sidebarLabelTooltip.attr('title', labelTooltipTitle).tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
- container: 'body'
+ container: 'body',
});
})
.catch(() => flash(__('Error saving label update.')));
@@ -132,34 +167,39 @@ export default class LabelsSelect {
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
- axios.get(labelUrl)
- .then((res) => {
- let data = _.chain(res.data).groupBy(function(label) {
- return label.title;
- }).map(function(label) {
- var color;
- color = _.map(label, function(dup) {
- return dup.color;
- });
- return {
- id: label[0].id,
- title: label[0].title,
- color: color,
- duplicate: color.length > 1
- };
- }).value();
+ labelUrl = $dropdown.attr('data-labels');
+ axios
+ .get(labelUrl)
+ .then(res => {
+ let data = _.chain(res.data)
+ .groupBy(function(label) {
+ return label.title;
+ })
+ .map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1,
+ };
+ })
+ .value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
- title: 'No Label'
+ title: 'No Label',
});
}
if (showAny) {
extraData.unshift({
isAny: true,
- title: 'Any Label'
+ title: 'Any Label',
});
}
if (extraData.length) {
@@ -176,11 +216,22 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
- var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
+ var $a,
+ $li,
+ color,
+ colorEl,
+ indeterminate,
+ removesAll,
+ selectedClass,
+ spacing,
+ i,
+ marked,
+ dropdownName,
+ dropdownValue;
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
- removesAll = label.id <= 0 || (label.id == null);
+ removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = $dropdown.data('indeterminate') || [];
marked = $dropdown.data('marked') || [];
@@ -200,9 +251,19 @@ export default class LabelsSelect {
} else {
if (this.id(label)) {
dropdownName = $dropdown.data('fieldName');
- dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
-
- if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+ dropdownValue = this.id(label)
+ .toString()
+ .replace(/'/g, "\\'");
+
+ if (
+ $form.find(
+ "input[type='hidden'][name='" +
+ dropdownName +
+ "'][value='" +
+ dropdownValue +
+ "']",
+ ).length
+ ) {
selectedClass.push('is-active');
}
}
@@ -213,16 +274,14 @@ export default class LabelsSelect {
}
if (label.duplicate) {
color = DropdownUtils.duplicateLabelColor(label.color);
- }
- else {
+ } else {
if (label.color != null) {
[color] = label.color;
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- }
- else {
+ } else {
colorEl = '';
}
// We need to identify which items are actually labels
@@ -235,7 +294,7 @@ export default class LabelsSelect {
return $li.html($a).prop('outerHTML');
},
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
filterable: true,
@@ -255,25 +314,21 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
return 'No Label';
- }
- else if (isSelected) {
+ } else if (isSelected) {
this.selected.push(title);
- }
- else if (!isSelected && title) {
+ } else if (!isSelected && title) {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
if (selectedLabels.length === 1) {
return selectedLabels;
- }
- else if (selectedLabels.length) {
+ } else if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
- labelCount: selectedLabels.length - 1
+ labelCount: selectedLabels.length - 1,
});
- }
- else {
+ } else {
return defaultLabel;
}
},
@@ -285,10 +340,9 @@ export default class LabelsSelect {
return label.id;
}
- if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title;
- }
- else {
+ } else {
return label.id;
}
},
@@ -310,13 +364,13 @@ export default class LabelsSelect {
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ selectedLabels = $dropdown
+ .closest('form')
+ .find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form'));
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
- }
- else {
+ } else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@@ -325,7 +379,7 @@ export default class LabelsSelect {
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function (clickEvent) {
+ clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
@@ -339,7 +393,8 @@ export default class LabelsSelect {
isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
- $dropdown.parent()
+ $dropdown
+ .parent()
.find('.dropdown-clear-active')
.removeClass('is-active');
}
@@ -367,28 +422,26 @@ export default class LabelsSelect {
e.preventDefault();
return;
- }
- else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
- }
- else if ($dropdown.hasClass('js-filter-submit')) {
+ } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- }
- else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if ($el.hasClass('is-active')) {
- boardsStore.detail.issue.labels.push(new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color[0],
- textColor: '#fff'
- }));
- }
- else {
+ boardsStore.detail.issue.labels.push(
+ new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff',
+ }),
+ );
+ } else {
var { labels } = boardsStore.detail.issue;
- labels = labels.filter(function (selectedLabel) {
+ labels = labels.filter(function(selectedLabel) {
return selectedLabel.id !== label.id;
});
boardsStore.detail.issue.labels = labels;
@@ -396,19 +449,16 @@ export default class LabelsSelect {
$loading.fadeIn();
- boardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ boardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
- }
- else if (handleClick) {
+ } else if (handleClick) {
e.preventDefault();
handleClick(label);
- }
- else {
+ } else {
if ($dropdown.hasClass('js-multiselect')) {
-
- }
- else {
+ } else {
return saveLabelData();
}
}
@@ -436,15 +486,17 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
- const tpl = _.template([
- '<% _.each(labels, function(label){ %>',
- '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
- '<%- label.title %>',
- '</span>',
- '</a>',
- '<% }); %>',
- ].join(''));
+ const tpl = _.template(
+ [
+ '<% _.each(labels, function(label){ %>',
+ '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
+ '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<%- label.title %>',
+ '</span>',
+ '</a>',
+ '<% }); %>',
+ ].join(''),
+ );
return tpl(tplData);
}
diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js
index efc4b2a8d94..ee71ae0e61a 100644
--- a/app/assets/javascripts/lib/utils/ace_utils.js
+++ b/app/assets/javascripts/lib/utils/ace_utils.js
@@ -1,6 +1,6 @@
/* global ace */
export default function getModeByFileExtension(path) {
- const modelist = ace.require("ace/ext/modelist");
+ const modelist = ace.require('ace/ext/modelist');
return modelist.getModeForPath(path).mode;
-};
+}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index afbab59055b..2ccc51c35f7 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
-*/
+ */
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 7d0c701fd70..bd263c75a3d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -7,8 +7,12 @@ export default class Members {
}
addListeners() {
- $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
- $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
+ $('.js-member-update-control')
+ .off('change')
+ .on('change', this.formSubmit.bind(this));
+ $('.js-edit-member-form')
+ .off('ajax:success')
+ .on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
@@ -28,7 +32,7 @@ export default class Members {
toggleLabel(selected, $el) {
return $el.text();
},
- clicked: (options) => {
+ clicked: options => {
this.formSubmit(null, options.$el);
},
});
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 42fb5c7177a..d32f39881dd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
-import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store';
+import boardsStore, {
+ boardStoreIssueSet,
+ boardStoreIssueDelete,
+} from './boards/stores/boards_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 1369b5820d5..90fe339e3de 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,7 +16,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -1293,10 +1293,10 @@ export default class Notes {
new Vue({
el,
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
render(createElement) {
- return createElement('skeleton-loading');
+ return createElement('gl-skeleton-loading');
},
});
}
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index d9e99603238..eaa0cded224 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -3,13 +3,13 @@ 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 { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
components: {
DiffFileHeader,
- SkeletonLoading,
+ GlSkeletonLoading,
},
props: {
discussion: {
@@ -143,7 +143,7 @@ export default {
class="line_content js-success-lazy-load"
>
<span></span>
- <skeleton-loading />
+ <gl-skeleton-loading />
<span></span>
</td>
</tr>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 15546949d07..affa2d1b574 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -60,7 +60,7 @@ export default {
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
- class="btn btn-default"
+ class="btn btn-default qa-discussion-filter"
data-toggle="dropdown"
aria-expanded="false"
>
@@ -78,6 +78,7 @@ export default {
>
<button
:class="{ 'is-active': filter.value === currentValue }"
+ class="qa-filter-options"
type="button"
@click="selectFilter(filter.value)"
>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index e707f44bf5a..df7ab4502a6 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -110,7 +110,7 @@ export default {
// Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
- // Add myself to the begining of the list so title will start with You.
+ // Add myself to the beginning of the list so title will start with You.
if (hasReactionByCurrentUser) {
namesToShow.unshift('You');
}
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
index ba1aafb9f00..5c5f38a3fb0 100644
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue';
-export default (store) => {
+export default store => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map(entry =>
- ({ title: entry, value: filterValues[entry] }));
+ const filters = Object.keys(filterValues).map(entry => ({
+ title: entry,
+ value: filterValues[entry],
+ }));
return new Vue({
el: discussionFilterEl,
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index 4532226aa07..bee6d4f0329 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => {
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
- // are they less than 10 minutes appart?
+ // are they less than 10 minutes apart?
if (timeDifferenceMinutes > 10) {
// reset counter
descriptionChangedTimes = 1;
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index d3b2656743d..ae0a8c74964 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
- saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
+ saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 2c683a39f42..9d19e4a095d 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -1,54 +1,66 @@
<script>
- import axios from '~/lib/utils/axios_utils';
- import createFlash from '~/flash';
- import GlModal from '~/vue_shared/components/gl_modal.vue';
- import { s__, sprintf } from '~/locale';
- import { visitUrl } from '~/lib/utils/url_utility';
- import eventHub from '../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
+import eventHub from '../event_hub';
- export default {
- components: {
- GlModal,
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ milestoneTitle: {
+ type: String,
+ required: true,
},
- props: {
- milestoneTitle: {
- type: String,
- required: true,
- },
- url: {
- type: String,
- required: true,
- },
- groupName: {
- type: String,
- required: true,
- },
+ url: {
+ type: String,
+ required: true,
},
- computed: {
- title() {
- return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
- },
- text() {
- return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
+ groupName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
+ milestoneTitle: this.milestoneTitle,
+ });
+ },
+ text() {
+ return sprintf(
+ s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
- },
+ This action cannot be reversed.`),
+ { milestoneTitle: this.milestoneTitle, groupName: this.groupName },
+ );
},
- methods: {
- onSubmit() {
- eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
- return axios.post(this.url, { params: { format: 'json' } })
- .then((response) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
- visitUrl(response.data.url);
- })
- .catch((error) => {
- eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
- createFlash(error);
+ },
+ methods: {
+ onSubmit() {
+ eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
+ return axios
+ .post(this.url, { params: { format: 'json' } })
+ .then(response => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: true,
});
- },
+ visitUrl(response.data.url);
+ })
+ .catch(error => {
+ eventHub.$emit('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: this.url,
+ successful: false,
+ });
+ createFlash(error);
+ });
},
- };
+ },
+};
</script>
<template>
<gl-modal
@@ -65,4 +77,3 @@
{{ text }}
</gl-modal>
</template>
-
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
deleted file mode 100644
index d4f34e32a48..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
-
-document.addEventListener('DOMContentLoaded', () => {
- initGkeDropdowns();
-});
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
new file mode 100644
index 00000000000..1b57c67f16b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
+ remainingTimeElements.forEach(
+ el =>
+ new Vue({
+ ...GlCountdown,
+ el,
+ propsData: {
+ endDateString: el.dateTime,
+ },
+ }),
+ );
+});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 52d66beefc9..a6bee49a6b1 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -64,7 +64,9 @@ export default class Project {
const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
Cookies.set(cookieKey, 'false');
- $(this).parents('.auto-devops-implicitly-enabled-banner').remove();
+ $(this)
+ .parents('.auto-devops-implicitly-enabled-banner')
+ .remove();
return e.preventDefault();
});
Project.projectSelectDropdown();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 8f5ac3d8082..15c6fb550c1 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new AjaxVariableList({
container: variableListEl,
- saveButton: variableListEl.querySelector('.js-secret-variables-save-button'),
+ saveButton: variableListEl.querySelector('.js-ci-variables-save-button'),
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a16f7e6b77c..c0ec7a5dc94 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -1,202 +1,200 @@
<script>
- import projectFeatureSetting from './project_feature_setting.vue';
- import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
- import projectSettingRow from './project_setting_row.vue';
- import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
- import { toggleHiddenClassBySelector } from '../external';
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue';
+import projectSettingRow from './project_setting_row.vue';
+import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
+import { toggleHiddenClassBySelector } from '../external';
- export default {
- components: {
- projectFeatureSetting,
- projectFeatureToggle,
- projectSettingRow,
- },
+export default {
+ components: {
+ projectFeatureSetting,
+ projectFeatureToggle,
+ projectSettingRow,
+ },
- props: {
- currentSettings: {
- type: Object,
- required: true,
- },
- canChangeVisibilityLevel: {
- type: Boolean,
- required: false,
- default: false,
- },
- allowedVisibilityOptions: {
- type: Array,
- required: false,
- default: () => [0, 10, 20],
- },
- lfsAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- registryAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- visibilityHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- lfsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- registryHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- pagesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesAccessControlEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- pagesHelpPath: {
- type: String,
- required: false,
- default: '',
- },
+ props: {
+ currentSettings: {
+ type: Object,
+ required: true,
+ },
+ canChangeVisibilityLevel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ allowedVisibilityOptions: {
+ type: Array,
+ required: false,
+ default: () => [0, 10, 20],
+ },
+ lfsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ registryAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visibilityHelpPath: {
+ type: String,
+ required: false,
+ default: '',
},
+ lfsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ registryHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pagesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesAccessControlEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pagesHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
- data() {
- const defaults = {
- visibilityOptions,
- visibilityLevel: visibilityOptions.PUBLIC,
- issuesAccessLevel: 20,
- repositoryAccessLevel: 20,
- mergeRequestsAccessLevel: 20,
- buildsAccessLevel: 20,
- wikiAccessLevel: 20,
- snippetsAccessLevel: 20,
- pagesAccessLevel: 20,
- containerRegistryEnabled: true,
- lfsEnabled: true,
- requestAccessEnabled: true,
- highlightChangesClass: false,
- };
+ data() {
+ const defaults = {
+ visibilityOptions,
+ visibilityLevel: visibilityOptions.PUBLIC,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ pagesAccessLevel: 20,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ requestAccessEnabled: true,
+ highlightChangesClass: false,
+ };
- return { ...defaults, ...this.currentSettings };
- },
+ return { ...defaults, ...this.currentSettings };
+ },
- computed: {
- featureAccessLevelOptions() {
- const options = [
- [10, 'Only Project Members'],
- ];
- if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
- options.push([20, 'Everyone With Access']);
- }
- return options;
- },
+ computed: {
+ featureAccessLevelOptions() {
+ const options = [[10, 'Only Project Members']];
+ if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
+ options.push([20, 'Everyone With Access']);
+ }
+ return options;
+ },
- repoFeatureAccessLevelOptions() {
- return this.featureAccessLevelOptions.filter(
- ([value]) => value <= this.repositoryAccessLevel,
- );
- },
+ repoFeatureAccessLevelOptions() {
+ return this.featureAccessLevelOptions.filter(
+ ([value]) => value <= this.repositoryAccessLevel,
+ );
+ },
- pagesFeatureAccessLevelOptions() {
- if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
- return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
- }
- return this.featureAccessLevelOptions;
- },
+ pagesFeatureAccessLevelOptions() {
+ if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
+ return this.featureAccessLevelOptions.concat([[30, 'Everyone']]);
+ }
+ return this.featureAccessLevelOptions;
+ },
- repositoryEnabled() {
- return this.repositoryAccessLevel > 0;
- },
+ repositoryEnabled() {
+ return this.repositoryAccessLevel > 0;
+ },
- visibilityLevelDescription() {
- return visibilityLevelDescriptions[this.visibilityLevel];
- },
+ visibilityLevelDescription() {
+ return visibilityLevelDescriptions[this.visibilityLevel];
},
+ },
- watch: {
- visibilityLevel(value, oldValue) {
- if (value === visibilityOptions.PRIVATE) {
- // when private, features are restricted to "only team members"
- this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
- this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
- this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
- this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
- this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
- this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
- if (this.pagesAccessLevel === 20) {
- // When from Internal->Private narrow access for only members
- this.pagesAccessLevel = 10;
- }
- this.highlightChanges();
- } else if (oldValue === visibilityOptions.PRIVATE) {
- // if changing away from private, make enabled features more permissive
- if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
- if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
- if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
- if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
- if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
- if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
- if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
- this.highlightChanges();
+ watch: {
+ visibilityLevel(value, oldValue) {
+ if (value === visibilityOptions.PRIVATE) {
+ // when private, features are restricted to "only team members"
+ this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
+ this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
+ this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
+ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
+ this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
+ this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ if (this.pagesAccessLevel === 20) {
+ // When from Internal->Private narrow access for only members
+ this.pagesAccessLevel = 10;
}
- },
+ this.highlightChanges();
+ } else if (oldValue === visibilityOptions.PRIVATE) {
+ // if changing away from private, make enabled features more permissive
+ if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
+ if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
+ if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
+ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
+ if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
+ if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
+ this.highlightChanges();
+ }
+ },
- repositoryAccessLevel(value, oldValue) {
- if (value < oldValue) {
- // sub-features cannot have more premissive access level
- this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
- this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
+ repositoryAccessLevel(value, oldValue) {
+ if (value < oldValue) {
+ // sub-features cannot have more premissive access level
+ this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
+ this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
- if (value === 0) {
- this.containerRegistryEnabled = false;
- this.lfsEnabled = false;
- }
- } else if (oldValue === 0) {
- this.mergeRequestsAccessLevel = value;
- this.buildsAccessLevel = value;
- this.containerRegistryEnabled = true;
- this.lfsEnabled = true;
+ if (value === 0) {
+ this.containerRegistryEnabled = false;
+ this.lfsEnabled = false;
}
- },
+ } else if (oldValue === 0) {
+ this.mergeRequestsAccessLevel = value;
+ this.buildsAccessLevel = value;
+ this.containerRegistryEnabled = true;
+ this.lfsEnabled = true;
+ }
+ },
- issuesAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
- },
+ issuesAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
+ },
- mergeRequestsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
- },
+ mergeRequestsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
+ },
- buildsAccessLevel(value, oldValue) {
- if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
- else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
- },
+ buildsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
+ },
- methods: {
- highlightChanges() {
- this.highlightChangesClass = true;
- this.$nextTick(() => {
- this.highlightChangesClass = false;
- });
- },
+ methods: {
+ highlightChanges() {
+ this.highlightChangesClass = true;
+ this.$nextTick(() => {
+ this.highlightChangesClass = false;
+ });
+ },
- visibilityAllowed(option) {
- return this.allowedVisibilityOptions.includes(option);
- },
+ visibilityAllowed(option) {
+ return this.allowedVisibilityOptions.includes(option);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index 75cb6374ad5..f970a5ebb64 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,8 +1,15 @@
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
+import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui';
export default {
+ components: {
+ GlModal,
+ },
+ directives: {
+ 'gl-modal': GlModalDirective,
+ },
props: {
deleteWikiUrl: {
type: String,
@@ -54,7 +61,7 @@ export default {
>
{{ __('Delete') }}
</button>
- <gl-ui-modal
+ <gl-modal
:title="title"
:ok-title="s__('WikiPageConfirmDelete|Delete page')"
:modal-id="modalId"
@@ -81,6 +88,6 @@ export default {
name="authenticity_token"
/>
</form>
- </gl-ui-modal>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index ea526cf1309..fcd8a54c9c1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -155,14 +155,6 @@ export default {
);
},
- shouldRenderPagination() {
- return (
- !this.isLoading &&
- this.state.pipelines.length &&
- this.state.pageInfo.total > this.state.pageInfo.perPage
- );
- },
-
emptyTabMessage() {
const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
@@ -232,36 +224,6 @@ export default {
this.setCommonData(resp.data.pipelines);
}
},
- /**
- * Handles URL and query parameter changes.
- * When the user uses the pagination or the tabs,
- * - update URL
- * - Make API request to the server with new parameters
- * - Update the polling function
- * - Update the internal state
- */
- updateContent(parameters) {
- this.updateInternalState(parameters);
-
- // fetch new data
- return this.service
- .getPipelines(this.requestData)
- .then(response => {
- this.isLoading = false;
- this.successCallback(response);
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- })
- .catch(() => {
- this.isLoading = false;
- this.errorCallback();
-
- // restart polling
- this.poll.restart({ data: this.requestData });
- });
- },
-
handleResetRunnersCache(endpoint) {
this.isResetCacheButtonLoading = true;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 16e69759091..a7507fb3b6f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -1,16 +1,17 @@
<script>
import { s__, sprintf } from '~/locale';
-import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
-import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
export default {
directives: {
tooltip,
},
components: {
- icon,
+ Icon,
+ GlCountdown,
},
props: {
actions: {
@@ -51,11 +52,6 @@ export default {
return !action.playable;
},
-
- remainingTime(action) {
- const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now();
- return formatTime(Math.max(0, remainingMilliseconds));
- },
},
};
</script>
@@ -100,7 +96,7 @@ export default {
class="pull-right"
>
<icon name="clock" />
- {{ remainingTime(action) }}
+ <gl-countdown :end-date-string="action.scheduled_at" />
</span>
</button>
</li>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index d40de95e051..e0f0434e03d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -1,13 +1,13 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
-import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
tooltip,
},
components: {
- icon,
+ Icon,
},
props: {
artifacts: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index cb14d4400d1..3339b5c13ed 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -88,25 +88,25 @@ export default {
class="table-section section-10 js-pipeline-status pipeline-status"
role="rowheader"
>
- Status
+ {{ s__('Pipeline|Status') }}
</div>
<div
class="table-section section-15 js-pipeline-info pipeline-info"
role="rowheader"
>
- Pipeline
+ {{ s__('Pipeline|Pipeline') }}
</div>
<div
class="table-section section-20 js-pipeline-commit pipeline-commit"
role="rowheader"
>
- Commit
+ {{ s__('Pipeline|Commit') }}
</div>
<div
class="table-section section-20 js-pipeline-stages pipeline-stages"
role="rowheader"
>
- Stages
+ {{ s__('Pipeline|Stages') }}
</div>
</div>
<pipelines-table-row-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4f89ee66023..026d533d10f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -261,7 +261,7 @@ export default {
class="table-mobile-header"
role="rowheader"
>
- Status
+ {{ s__('Pipeline|Status') }}
</div>
<div class="table-mobile-content">
<ci-badge
@@ -279,8 +279,9 @@ export default {
<div class="table-section section-20">
<div
class="table-mobile-header"
- role="rowheader">
- Commit
+ role="rowheader"
+ >
+ {{ s__('Pipeline|Commit') }}
</div>
<div class="table-mobile-content">
<commit-component
@@ -298,8 +299,9 @@ export default {
<div class="table-section section-wrap section-20 stage-cell">
<div
class="table-mobile-header"
- role="rowheader">
- Stages
+ role="rowheader"
+ >
+ {{ s__('Pipeline|Stages') }}
</div>
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index cd43d78de40..bed690200b8 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -60,7 +60,7 @@ export default {
class="table-mobile-header"
role="rowheader"
>
- Duration
+ {{ s__('Pipeline|Duration') }}
</div>
<div class="table-mobile-content">
<p
@@ -87,7 +87,8 @@ export default {
v-tooltip
:title="tooltipTitle(finishedTime)"
data-placement="top"
- data-container="body">
+ data-container="body"
+ >
{{ timeFormated(finishedTime) }}
</time>
</p>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 8929b397f6c..85781f548c6 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -23,6 +23,15 @@ export default {
hasMadeRequest: false,
};
},
+ computed: {
+ shouldRenderPagination() {
+ return (
+ !this.isLoading &&
+ this.state.pipelines.length &&
+ this.state.pageInfo.total > this.state.pageInfo.perPage
+ );
+ },
+ },
beforeMount() {
this.poll = new Poll({
resource: this.service,
@@ -65,6 +74,35 @@ export default {
this.poll.stop();
},
methods: {
+ /**
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
+ */
+ updateContent(parameters) {
+ this.updateInternalState(parameters);
+
+ // fetch new data
+ return this.service
+ .getPipelines(this.requestData)
+ .then(response => {
+ this.isLoading = false;
+ this.successCallback(response);
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ });
+ },
updateTable() {
// Cancel ongoing request
if (this.isMakingRequest) {
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebe18b47e4e..998554d1be5 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false;
-const deriveProjectPathFromUrl = ($projectImportUrl) => {
- const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path');
+const deriveProjectPathFromUrl = $projectImportUrl => {
+ const $currentProjectPath = $projectImportUrl
+ .parents('.toggle-import-form')
+ .find('#project_path');
if (hasUserDefinedProjectPath) {
return;
}
@@ -52,9 +54,11 @@ const bindEvents = () => {
return;
}
- $('.how_to_import_link').on('click', (e) => {
+ $('.how_to_import_link').on('click', e => {
e.preventDefault();
- $(e.currentTarget).next('.modal').show();
+ $(e.currentTarget)
+ .next('.modal')
+ .show();
});
$('.modal-header .close').on('click', () => {
@@ -63,15 +67,21 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
- $('.btn_import_gitlab_project')
- .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`);
+ $('.btn_import_gitlab_project').attr(
+ 'href',
+ `${importHref}?namespace_id=${$(
+ '#project_namespace_id',
+ ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
+ );
});
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
- .on('click', (e) => { e.preventDefault(); })
+ .on('click', e => {
+ e.preventDefault();
+ })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'bottom',
@@ -79,13 +89,15 @@ const bindEvents = () => {
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
- $(document).on('click.popover touchstart.popover', (event) => {
+ $(document).on('click.popover touchstart.popover', event => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
- const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
+ const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find(
+ '.js-select-on-focus',
+ );
addSelectOnFocusBehaviour(target);
target.focus();
@@ -117,16 +129,18 @@ const bindEvents = () => {
const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text);
- $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
+ $(selectedTemplate.icon)
+ .clone()
+ .addClass('d-block')
+ .appendTo($selectedIcon);
const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus();
- $activeTabProjectName
- .keyup(() => {
- onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
- hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
- });
+ $activeTabProjectName.keyup(() => {
+ onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
+ hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
+ });
}
$useTemplateBtn.on('change', chooseTemplate);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 6b3753f7966..225e21ad322 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
}
};
-Sidebar.prototype.removeListeners = function () {
+Sidebar.prototype.removeListeners = function() {
this.sidebar.off('click', '.sidebar-collapsed-icon');
this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown');
@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
- return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+ return $(document)
+ .off('click', '.js-issuable-todo')
+ .on('click', '.js-issuable-todo', this.toggleTodo);
};
-Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
+Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
- $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
+ $('.layout-page')
+ .removeClass('right-sidebar-expanded')
+ .addClass('right-sidebar-collapsed');
} else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
- $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('aside.right-sidebar')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
+ $('.layout-page')
+ .removeClass('right-sidebar-collapsed')
+ .addClass('right-sidebar-expanded');
}
$this.attr('data-original-title', tooltipLabel);
if (!triggered) {
- Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
+ Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
};
@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
- url = "" + ($this.attr('data-delete-path'));
+ url = '' + $this.attr('data-delete-path');
} else {
- url = "" + ($this.data('url'));
+ url = '' + $this.data('url');
}
$this.tooltip('hide');
- $('.js-issuable-todo').disable().addClass('is-loading');
+ $('.js-issuable-todo')
+ .disable()
+ .addClass('is-loading');
axios[ajaxType](url, {
issuable_id: $this.data('issuableId'),
issuable_type: $this.data('issuableType'),
- }).then(({ data }) => {
- this.todoUpdateDone(data);
- }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
+ })
+ .then(({ data }) => {
+ this.todoUpdateDone(data);
+ })
+ .catch(() =>
+ flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
+ );
};
Sidebar.prototype.todoUpdateDone = function(data) {
@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner');
- $el.removeClass('is-loading')
+ $el
+ .removeClass('is-loading')
.enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath)
@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar.prototype.sidebarDropdownLoading = function(e) {
var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
i = $sidebarCollapsedIcon.find('i');
$loading = $('<i class="fa fa-spinner fa-spin"></i>');
@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function(e) {
var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ $sidebarCollapsedIcon = $(this)
+ .closest('.block')
+ .find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove();
i = $sidebarCollapsedIcon.find('i');
@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
};
Sidebar.prototype.getBlock = function(name) {
- return this.sidebar.find(".block." + name);
+ return this.sidebar.find('.block.' + name);
};
export default Sidebar;
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 7bde4860973..17def77b2d7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -226,7 +226,7 @@ export class SearchAutocomplete {
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
- url: `/search?search=${term}`,
+ url: `${gon.relative_url_root}/search?search=${term}`,
});
if (template) {
@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon,
text: term,
template,
- url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
+ url: `${
+ gon.relative_url_root
+ }/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
});
}
}
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 43f0b6651b9..8950ae31627 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
+import { GlModal } from '@gitlab-org/gitlab-ui';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
@@ -13,6 +14,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
+ GlModal,
},
props: {
currentEmoji: {
@@ -152,7 +154,7 @@ export default {
</script>
<template>
- <gl-ui-modal
+ <gl-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
@@ -237,5 +239,5 @@ export default {
</div>
</div>
</div>
- </gl-ui-modal>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index dd155c133ce..f1ea6aacdb2 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -74,8 +74,8 @@ export default {
}
if (!this.users.length) {
- const emptyTooltipLabel = this.issuableType === 'issue' ?
- __('Assignee(s)') : __('Assignee');
+ const emptyTooltipLabel =
+ this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
@@ -248,4 +248,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 448c8fc3602..b6151aa6c64 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -1,74 +1,74 @@
<script>
- import { __ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import toggleButton from '~/vue_shared/components/toggle_button.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import eventHub from '../../event_hub';
+import { __ } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import eventHub from '../../event_hub';
- const ICON_ON = 'notifications';
- const ICON_OFF = 'notifications-off';
- const LABEL_ON = __('Notifications on');
- const LABEL_OFF = __('Notifications off');
+const ICON_ON = 'notifications';
+const ICON_OFF = 'notifications-off';
+const LABEL_ON = __('Notifications on');
+const LABEL_OFF = __('Notifications off');
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ toggleButton,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- icon,
- toggleButton,
+ subscribed: {
+ type: Boolean,
+ required: false,
+ default: null,
},
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- subscribed: {
- type: Boolean,
- required: false,
- default: null,
- },
- id: {
- type: Number,
- required: false,
- default: null,
- },
+ id: {
+ type: Number,
+ required: false,
+ default: null,
},
- computed: {
- showLoadingState() {
- return this.subscribed === null;
- },
- notificationIcon() {
- return this.subscribed ? ICON_ON : ICON_OFF;
- },
- notificationTooltip() {
- return this.subscribed ? LABEL_ON : LABEL_OFF;
- },
+ },
+ computed: {
+ showLoadingState() {
+ return this.subscribed === null;
},
- methods: {
- /**
- * We need to emit this event on both component & eventHub
- * for 2 dependencies;
- *
- * 1. eventHub: This component is used in Issue Boards sidebar
- * where component template is part of HAML
- * and event listeners are tied to app's eventHub.
- * 2. Component: This compone is also used in Epics in EE
- * where listeners are tied to component event.
- */
- toggleSubscription() {
- // App's eventHub event emission.
- eventHub.$emit('toggleSubscription', this.id);
+ notificationIcon() {
+ return this.subscribed ? ICON_ON : ICON_OFF;
+ },
+ notificationTooltip() {
+ return this.subscribed ? LABEL_ON : LABEL_OFF;
+ },
+ },
+ methods: {
+ /**
+ * We need to emit this event on both component & eventHub
+ * for 2 dependencies;
+ *
+ * 1. eventHub: This component is used in Issue Boards sidebar
+ * where component template is part of HAML
+ * and event listeners are tied to app's eventHub.
+ * 2. Component: This compone is also used in Epics in EE
+ * where listeners are tied to component event.
+ */
+ toggleSubscription() {
+ // App's eventHub event emission.
+ eventHub.$emit('toggleSubscription', this.id);
- // Component event emission.
- this.$emit('toggleSubscription', this.id);
- },
- onClickCollapsedIcon() {
- this.$emit('toggleSidebar');
- },
+ // Component event emission.
+ this.$emit('toggleSubscription', this.id);
+ },
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index e74912d628f..b145e5dc5e2 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,9 +1,13 @@
<script>
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
+import { GlProgressBar } from '@gitlab-org/gitlab-ui';
export default {
name: 'TimeTrackingComparisonPane',
+ components: {
+ GlProgressBar,
+ },
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index d9ca5e46770..3e040ec8428 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -39,9 +39,10 @@ export default class SidebarMediator {
}
fetch() {
- return this.service.get()
+ return this.service
+ .get()
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.processFetchedData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
- return this.service.toggleSubscription()
+ return this.service
+ .toggleSubscription()
.then(() => {
this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false);
})
- .catch((err) => {
+ .catch(err => {
this.store.setFetchingState('subscriptions', false);
throw err;
});
}
fetchAutocompleteProjects(searchTerm) {
- return this.service.getProjectsAutocomplete(searchTerm)
+ return this.service
+ .getProjectsAutocomplete(searchTerm)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects;
});
}
moveIssue() {
- return this.service.moveIssue(this.store.moveToProjectId)
+ return this.service
+ .moveIssue(this.store.moveToProjectId)
.then(response => response.json())
- .then((data) => {
+ .then(data => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 6c87287a4c4..57c52a2016a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -2,6 +2,7 @@
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
+import { __ } from '~/locale';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -9,6 +10,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import createFlash from '../../flash';
import MemoryUsage from './memory_usage.vue';
import StatusIcon from './mr_widget_status_icon.vue';
+import ReviewAppLink from './review_app_link.vue';
import MRWidgetService from '../services/mr_widget_service';
export default {
@@ -20,6 +22,7 @@ export default {
Icon,
TooltipOnTruncate,
FilteredSearchDropdown,
+ ReviewAppLink,
},
directives: {
tooltip,
@@ -31,6 +34,11 @@ export default {
required: true,
},
},
+ deployedTextMap: {
+ running: __('Deploying to'),
+ success: __('Deployed to'),
+ failed: __('Failed to deploy to'),
+ },
data() {
const features = window.gon.features || {};
return {
@@ -54,10 +62,19 @@ export default {
hasMetrics() {
return !!this.deployment.metrics_url;
},
+ deployedText() {
+ return this.$options.deployedTextMap[this.deployment.status];
+ },
+ shouldRenderDropdown() {
+ return (
+ this.enableCiEnvironmentsStatusChanges &&
+ (this.deployment.changes && this.deployment.changes.length > 0)
+ );
+ },
},
methods: {
stopEnvironment() {
- const msg = 'Are you sure you want to stop this environment?';
+ const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
@@ -87,10 +104,10 @@ export default {
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
- <div class="deployment-info">
+ <div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span>
- Deployed to
+ {{ deployedText }}
</span>
<tooltip-on-truncate
:title="deployment.name"
@@ -124,7 +141,7 @@ export default {
<div>
<template v-if="hasExternalUrls">
<filtered-search-dropdown
- v-if="enableCiEnvironmentsStatusChanges"
+ v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
:main-action-link="deployment.external_url"
@@ -134,18 +151,10 @@ export default {
slot="mainAction"
slot-scope="slotProps"
>
- <a
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-url inline"
- :class="slotProps.className"
- >
- <span>
- {{ __('View app') }}
- <icon name="external-link" />
- </span>
- </a>
+ <review-app-link
+ :link="deployment.external_url"
+ :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
+ />
</template>
<template
@@ -168,18 +177,11 @@ export default {
</a>
</template>
</filtered-search-dropdown>
- <a
+ <review-app-link
v-else
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
- >
- <span>
- {{ __('View app') }}
- <icon name="external-link" />
- </span>
- </a>
+ :link="deployment.external_url"
+ css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
+ />
</template>
<loading-button
v-if="deployment.stop_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index fee41b239e8..8bcabc10225 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,5 +1,6 @@
<script>
/* eslint-disable vue/require-default-prop */
+import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -36,6 +37,10 @@ export default {
type: String,
required: false,
},
+ troubleshootingDocsPath: {
+ type: String,
+ required: true,
+ },
},
computed: {
hasPipeline() {
@@ -57,6 +62,17 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
+ errorText() {
+ return sprintf(
+ __(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ ),
+ {
+ linkStart: `<a href="${this.troubleshootingDocsPath}">`,
+ linkEnd: '</a>',
+ },
+ );
+ },
},
};
</script>
@@ -77,8 +93,10 @@ export default {
name="status_failed_borderless"
/>
</div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
+ <div
+ class="media-body"
+ v-html="errorText"
+ >
</div>
</template>
<template v-else-if="hasPipeline">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
new file mode 100644
index 00000000000..b007d4f4dcb
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -0,0 +1,30 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ link: {
+ type: String,
+ required: true,
+ },
+ cssClass: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <a
+ :href="link"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :class="cssClass"
+ >
+ {{ __('View app') }}
+ <icon name="external-link" />
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index c8ad2aa30a6..e7baecbcde4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -71,7 +71,12 @@ export default {
return defaultClass;
},
iconClass() {
- if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) {
+ if (
+ this.status === 'failed' ||
+ !this.commitMessage.length ||
+ !this.mr.isMergeAllowed ||
+ this.mr.preventMerge
+ ) {
return 'warning';
}
return 'success';
@@ -90,10 +95,12 @@ export default {
},
isMergeButtonDisabled() {
const { commitMessage } = this;
- return Boolean(!commitMessage.length
- || !this.shouldShowMergeControls()
- || this.isMakingRequest
- || this.mr.preventMerge);
+ return Boolean(
+ !commitMessage.length ||
+ !this.shouldShowMergeControls() ||
+ this.isMakingRequest ||
+ this.mr.preventMerge,
+ );
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
@@ -140,9 +147,10 @@ export default {
};
this.isMakingRequest = true;
- this.service.merge(options)
+ this.service
+ .merge(options)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') {
@@ -167,9 +175,10 @@ export default {
});
},
handleMergePolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
@@ -205,9 +214,10 @@ export default {
});
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
- this.service.poll()
+ this.service
+ .poll()
.then(res => res.data)
- .then((data) => {
+ .then(data => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (data.source_branch_exists) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 5d9f7cebcf2..063d1e15544 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,4 +1,6 @@
<script>
+import _ from 'underscore';
+import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
@@ -80,6 +82,7 @@ export default {
const service = this.createService(store);
return {
mr: store,
+ state: store.state,
service,
};
},
@@ -103,6 +106,17 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
+ shouldRenderMergedPipeline() {
+ return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
+ },
+ },
+ watch: {
+ state(newVal, oldVal) {
+ if (newVal !== oldVal && this.shouldRenderMergedPipeline) {
+ // init polling
+ this.initPostMergeDeploymentsPolling();
+ }
+ },
},
created() {
this.initPolling();
@@ -112,11 +126,19 @@ export default {
mounted() {
this.setFaviconHelper();
this.initDeploymentsPolling();
+
+ if (this.shouldRenderMergedPipeline) {
+ this.initPostMergeDeploymentsPolling();
+ }
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
this.pollingInterval.destroy();
this.deploymentsInterval.destroy();
+
+ if (this.postMergeDeploymentsInterval) {
+ this.postMergeDeploymentsInterval.destroy();
+ }
},
methods: {
createService(store) {
@@ -146,7 +168,13 @@ export default {
cb.call(null, data);
}
})
- .catch(() => createFlash('Something went wrong. Please try again.'));
+ .catch(() => createFlash(__('Something went wrong. Please try again.')));
+ },
+ setFaviconHelper() {
+ if (this.mr.ciStatusFaviconPath) {
+ return setFaviconOverlay(this.mr.ciStatusFaviconPath);
+ }
+ return Promise.resolve();
},
initPolling() {
this.pollingInterval = new SmartInterval({
@@ -158,8 +186,14 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new SmartInterval({
- callback: this.fetchDeployments,
+ this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments);
+ },
+ initPostMergeDeploymentsPolling() {
+ this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments);
+ },
+ deploymentsPoll(callback) {
+ return new SmartInterval({
+ callback,
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
@@ -167,26 +201,33 @@ export default {
immediateExecution: true,
});
},
- setFaviconHelper() {
- if (this.mr.ciStatusFaviconPath) {
- return setFaviconOverlay(this.mr.ciStatusFaviconPath);
- }
- return Promise.resolve();
+ fetchDeployments(target) {
+ return this.service.fetchDeployments(target);
},
- fetchDeployments() {
- return this.service
- .fetchDeployments()
- .then(res => res.data)
- .then(data => {
+ fetchPreMergeDeployments() {
+ return this.fetchDeployments()
+ .then(({ data }) => {
if (data.length) {
this.mr.deployments = data;
}
})
- .catch(() => {
- createFlash(
- 'Something went wrong while fetching the environments for this merge request. Please try again.',
- );
- });
+ .catch(() => this.throwDeploymentsError());
+ },
+ fetchPostMergeDeployments() {
+ return this.fetchDeployments('merge_commit')
+ .then(({ data }) => {
+ if (data.length) {
+ this.mr.postMergeDeployments = data;
+ }
+ })
+ .catch(() => this.throwDeploymentsError());
+ },
+ throwDeploymentsError() {
+ createFlash(
+ __(
+ 'Something went wrong while fetching the environments for this merge request. Please try again.',
+ ),
+ );
},
fetchActionsContent() {
this.service
@@ -199,7 +240,7 @@ export default {
Project.initRefSwitcher();
}
})
- .catch(() => createFlash('Something went wrong. Please try again.'));
+ .catch(() => createFlash(__('Something went wrong. Please try again.')));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
@@ -264,10 +305,12 @@ export default {
:has-ci="mr.hasCI"
:source-branch="mr.sourceBranch"
:source-branch-link="mr.sourceBranchLink"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<deployment
v-for="deployment in mr.deployments"
- :key="deployment.id"
+ :key="`pre-merge-deploy-${deployment.id}`"
+ class="js-pre-merge-deploy"
:deployment="deployment"
/>
<div class="mr-section-container">
@@ -308,5 +351,23 @@ export default {
<mr-widget-merge-help />
</div>
</div>
+
+ <template v-if="shouldRenderMergedPipeline">
+ <mr-widget-pipeline
+ class="js-post-merge-pipeline prepend-top-default"
+ :pipeline="mr.mergePipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ :source-branch="mr.targetBranch"
+ :source-branch-link="mr.targetBranch"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ />
+ <deployment
+ v-for="postMergeDeployment in mr.postMergeDeployments"
+ :key="`post-merge-deploy-${postMergeDeployment.id}`"
+ :deployment="postMergeDeployment"
+ class="js-post-deployment"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index fecbfec2214..0bb70bfd658 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -21,8 +21,12 @@ export default class MRWidgetService {
return axios.delete(this.endpoints.sourceBranchPath);
}
- fetchDeployments() {
- return axios.get(this.endpoints.ciEnvironmentsStatusPath);
+ fetchDeployments(targetParam) {
+ return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
+ params: {
+ environment_target: targetParam,
+ },
+ });
}
poll() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e6655914700..5c9a7133a6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -18,6 +18,7 @@ export default class MergeRequestStore {
this.squash = data.squash;
this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
+ this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid;
@@ -32,7 +33,9 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
+ this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
+ this.postMergeDeployments = this.postMergeDeployments || [];
this.initRebase(data);
if (data.issues_links) {
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 13bca99dcb3..151eee75d44 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -13,7 +13,7 @@ export default {
},
props: {
/**
- * Indicates the existance of a tag.
+ * Indicates the existence of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index a07d63a495d..c78b96695cf 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -1,11 +1,11 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import Icon from '../../icon.vue';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
Icon,
},
props: {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 807e049caf6..419987d2c50 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -2,14 +2,14 @@
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import $ from 'jquery';
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
const { CancelToken } = axios;
let axiosSource;
export default {
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
props: {
content: {
@@ -81,7 +81,7 @@ export default {
<div
ref="markdown-preview"
class="md md-previewer">
- <skeleton-loading v-if="isLoading" />
+ <gl-skeleton-loading v-if="isLoading" />
<div
v-else
v-html="previewContent">
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
index 460fa6ad72e..388a2f4ca36 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
@@ -56,12 +56,14 @@ export default {
filteredResults() {
if (this.filter !== '') {
return this.items.filter(
- item => item[this.filterKey] && item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
+ item =>
+ item[this.filterKey] &&
+ item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
);
}
return this.items.slice(0, this.visibleItems);
- }
+ },
},
mounted() {
/**
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 26f9d5ddc91..cddebfae115 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -8,7 +8,7 @@ let iconValidator = () => true;
*/
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line global-require
- const data = require('@gitlab-org/gitlab-svgs/dist/icons.json');
+ const data = require('@gitlab/svgs/dist/icons.json');
const { icons } = data;
iconValidator = value => {
if (icons.includes(value)) {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 3ddb39730c4..27e3f314dd3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,17 +1,17 @@
<script>
import $ from 'jquery';
-import Tooltip from '../../directives/tooltip';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
export default {
- directives: {
- Tooltip,
- },
components: {
ToolbarButton,
Icon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
previewMarkdown: {
type: Boolean,
@@ -147,7 +147,7 @@ export default {
icon="table"
/>
<button
- v-tooltip
+ v-gl-tooltip
aria-label="Go full screen"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index feb7b8f227e..b0a93794013 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,9 +1,9 @@
<script>
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab-org/gitlab-ui';
export default {
components: {
- 'gl-link': Link,
+ GlLink,
},
props: {
markdownDocsPath: {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 3e89e1c1e75..91d0bbfc21c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,13 +1,13 @@
<script>
-import tooltip from '../../directives/tooltip';
-import icon from '../icon.vue';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import Icon from '../icon.vue';
export default {
components: {
- icon,
+ Icon,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
buttonTitle: {
@@ -43,7 +43,7 @@ export default {
<template>
<button
- v-tooltip
+ v-gl-tooltip
:data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock"
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 1d9c9220469..f56414c3c63 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,10 +1,10 @@
<script>
-import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
+import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui';
export default {
name: 'SkeletonNote',
components: {
- SkeletonLoading,
+ GlSkeletonLoading,
},
};
</script>
@@ -17,7 +17,7 @@ export default {
<div class="timeline-content">
<div class="note-header"></div>
<div class="note-body">
- <skeleton-loading />
+ <gl-skeleton-loading />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue
index 1f2a679c145..89dcf049f6e 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_links.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue
@@ -1,7 +1,11 @@
<script>
+import { GlPagination } from '@gitlab-org/gitlab-ui';
import { s__ } from '../../locale';
export default {
+ components: {
+ GlPagination,
+ },
props: {
change: {
type: Function,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
index 7f1eb6bcec4..5841db52704 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -1,34 +1,50 @@
<script>
- export default {
- name: 'CollapsedCalendarIcon',
- props: {
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- text: {
- type: String,
- required: false,
- default: '',
- },
- showIcon: {
- type: Boolean,
- required: false,
- default: true,
- },
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'CollapsedCalendarIcon',
+ directives: {
+ tooltip,
+ },
+ props: {
+ containerClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- click() {
- this.$emit('click');
- },
+ },
+ methods: {
+ click() {
+ this.$emit('click');
},
- };
+ },
+};
</script>
<template>
<div
+ v-tooltip
:class="containerClass"
+ :title="tooltipText"
+ data-container="body"
+ data-placement="left"
+ data-html="true"
+ data-boundary="viewport"
@click="click"
>
<i
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
index dac438a702d..174c29809ac 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -1,88 +1,87 @@
<script>
- import { dateInWords } from '../../../lib/utils/datetime_utility';
- import toggleSidebar from './toggle_sidebar.vue';
- import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
+import { __ } from '~/locale';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
+import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
- export default {
- name: 'SidebarCollapsedGroupedDatePicker',
- components: {
- toggleSidebar,
- collapsedCalendarIcon,
+export default {
+ name: 'SidebarCollapsedGroupedDatePicker',
+ components: {
+ collapsedCalendarIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- collapsed: {
- type: Boolean,
- required: false,
- default: true,
- },
- showToggleSidebar: {
- type: Boolean,
- required: false,
- default: false,
- },
- minDate: {
- type: Date,
- required: false,
- default: null,
- },
- maxDate: {
- type: Date,
- required: false,
- default: null,
- },
- disableClickableIcons: {
- type: Boolean,
- required: false,
- default: false,
- },
+ minDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- computed: {
- hasMinAndMaxDates() {
- return this.minDate && this.maxDate;
- },
- hasNoMinAndMaxDates() {
- return !this.minDate && !this.maxDate;
- },
- showMinDateBlock() {
- return this.minDate || this.hasNoMinAndMaxDates;
- },
- showFromText() {
- return !this.maxDate && this.minDate;
- },
- iconClass() {
- const disabledClass = this.disableClickableIcons ? 'disabled' : '';
- return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
- },
+ maxDate: {
+ type: Date,
+ required: false,
+ default: null,
},
- methods: {
- toggleSidebar() {
- this.$emit('toggleCollapse');
- },
- dateText(dateType = 'min') {
- const date = this[`${dateType}Date`];
- const dateWords = dateInWords(date, true);
- const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+ disableClickableIcons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasMinAndMaxDates() {
+ return this.minDate && this.maxDate;
+ },
+ hasNoMinAndMaxDates() {
+ return !this.minDate && !this.maxDate;
+ },
+ showMinDateBlock() {
+ return this.minDate || this.hasNoMinAndMaxDates;
+ },
+ showFromText() {
+ return !this.maxDate && this.minDate;
+ },
+ iconClass() {
+ const disabledClass = this.disableClickableIcons ? 'disabled' : '';
+ return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
+ },
+ },
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleCollapse');
+ },
+ dateText(dateType = 'min') {
+ const date = this[`${dateType}Date`];
+ const dateWords = dateInWords(date, true);
+ const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
+
+ return date ? parsedDateWords : __('None');
+ },
+ tooltipText(dateType = 'min') {
+ const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
+ const date = this[`${dateType}Date`];
+ const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date);
+ const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
- return date ? parsedDateWords : 'None';
- },
+ if (date) {
+ return [defaultText, dateText].join('<br />');
+ }
+ return __('Start and due date');
},
- };
+ },
+};
</script>
<template>
<div class="block sidebar-grouped-item">
- <div
- v-if="showToggleSidebar"
- class="issuable-sidebar-header"
- >
- <toggle-sidebar
- :collapsed="collapsed"
- @toggle="toggleSidebar"
- />
- </div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
+ :tooltip-text="tooltipText('min')"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
@@ -99,7 +98,7 @@
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
- :show-icon="!minDate"
+ :tooltip-text="tooltipText('max')"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index af297f3c408..0d5fc07e6e3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -14,7 +14,10 @@ export default {
},
computed: {
labelsList() {
- const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+ const labelsString = this.labels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
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 14cb44b8619..86c7498a092 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,14 +17,14 @@
*/
-import { Link } from '@gitlab-org/gitlab-ui';
+import { GlLink } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
components: {
- 'gl-link': Link,
+ GlLink,
userAvatarImage,
},
directives: {
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
index 67a1632269e..f9e3f3df0cc 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -14,7 +14,14 @@ export default {
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
- this.updateContent({ scope: this.scope, page: Number(page).toString() });
+ const params = {
+ page: Number(page).toString(),
+ };
+
+ if (this.scope) {
+ params.scope = this.scope;
+ }
+ this.updateContent(params);
},
updateInternalState(parameters) {
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 2e7f25d975e..6f103e4e89a 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -322,15 +322,15 @@
width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
+ height: $toggle-sidebar-height;
bottom: 0;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
background-color: $gray-light;
border: 0;
border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
- line-height: 1;
svg {
margin-right: 8px;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cdfad30e7ca..dca89981d81 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -158,7 +158,7 @@
color: $gl-text-color;
outline: 0;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
@@ -184,7 +184,7 @@
text-align: left;
width: 100%;
- // make sure the text color is not overriden
+ // make sure the text color is not overridden
&.text-danger {
color: $brand-danger;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1c84baf68ed..9837b1a6bd0 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -250,6 +250,101 @@
max-width: 100%;
}
+/*
+* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
+*/
+@mixin build-trace {
+ background: $black;
+ color: $gray-darkest;
+ white-space: pre;
+ overflow-x: auto;
+ font-size: 12px;
+ border-radius: 0;
+ border: 0;
+ padding: $grid-size;
+
+ .bash {
+ display: block;
+ }
+
+ &.build-trace-rounded {
+ border-radius: $border-radius-base;
+ }
+}
+
+@mixin build-trace-top-bar($height) {
+ height: $height;
+ min-height: $height;
+ background: $gray-light;
+ border: 1px solid $border-color;
+ color: $gl-text-color;
+ position: sticky;
+ position: -webkit-sticky;
+ top: $header-height;
+ padding: $grid-size;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+}
+
+/*
+* Mixin that handles the position of the controls placed on the top bar
+*/
+@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') {
+ display: flex;
+ font-size: $control-font-size;
+ justify-content: $flex-direction;
+ align-items: center;
+ align-self: baseline;
+ @if $with-grow {
+ flex-grow: $flex-grow-size;
+ }
+
+ svg {
+ width: 15px;
+ height: 15px;
+ display: $svg-display;
+ fill: $gl-text-color;
+ top: $svg-top;
+ }
+
+ .controllers-buttons {
+ color: $gl-text-color;
+ margin: 0 $grid-size;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .btn-scroll.animate {
+ .first-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ animation-delay: 0.3s;
+ }
+
+ .second-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ animation-delay: 0.2s;
+ }
+
+ .third-triangle {
+ animation: blinking-scroll-button 1s ease infinite;
+ }
+
+ &:disabled {
+ opacity: 1;
+ }
+ }
+
+ .btn-scroll:disabled,
+ .btn-refresh:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+ }
+}
+
@mixin build-loader-animation {
position: relative;
white-space: initial;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f47dfe1b563..de9e7c37695 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -1,6 +1,6 @@
// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
// please check nav.scss
-.nav-links {
+.nav-links:not(.quick-links) {
display: flex;
padding: 0;
margin: 0;
@@ -106,7 +106,7 @@
display: inline-block;
float: right;
text-align: right;
- padding: 11px 0;
+ padding: $gl-padding-8 0;
margin-bottom: 0;
> .btn,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ad66a0365ed..19eee4e4aba 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,6 +10,7 @@ $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
+$toggle-sidebar-height: 48px;
/*
* Color schema
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 227f49ec595..1449723de52 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -50,35 +50,13 @@
position: relative;
}
- .build-trace {
- background: $black;
- color: $gray-darkest;
- white-space: pre;
- overflow-x: auto;
- font-size: 12px;
- border-radius: 0;
- border: 0;
- padding: $grid-size;
-
- .bash {
- display: block;
- }
- &.build-trace-rounded {
- border-radius: $border-radius-base;
- }
+ .build-trace {
+ @include build-trace();
}
.top-bar {
- height: 35px;
- min-height: 35px;
- background: $gray-light;
- border: 1px solid $border-color;
- color: $gl-text-color;
- position: sticky;
- position: -webkit-sticky;
- top: $header-height;
- padding: $grid-size;
+ @include build-trace-top-bar(35px);
&.affix {
top: $header-height;
@@ -116,49 +94,7 @@
}
.controllers {
- display: flex;
- justify-content: center;
- align-items: center;
-
- svg {
- height: 15px;
- display: block;
- fill: $gl-text-color;
- }
-
- .controllers-buttons {
- color: $gl-text-color;
- margin: 0 $grid-size;
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- .btn-scroll.animate {
- .first-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- animation-delay: 0.3s;
- }
-
- .second-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- animation-delay: 0.2s;
- }
-
- .third-triangle {
- animation: blinking-scroll-button 1s ease infinite;
- }
-
- &:disabled {
- opacity: 1;
- }
- }
-
- .btn-scroll:disabled {
- opacity: 0.35;
- cursor: not-allowed;
- }
+ @include build-controllers(15px, center, false, 0, inline, 0);
}
}
@@ -183,12 +119,8 @@
}
.with-performance-bar .build-page {
- .top-bar {
+ .top-bar.affix {
top: $header-height + $performance-bar-height;
-
- &.affix {
- top: $header-height + $performance-bar-height;
- }
}
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index fdd3b4126ff..e3226c86b0b 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -33,21 +33,21 @@ class Admin::AppearancesController < Admin::ApplicationController
@appearance.save
- redirect_to admin_appearances_path, notice: 'Logo was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Logo was successfully removed.'
end
def header_logos
@appearance.remove_header_logo!
@appearance.save
- redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Header logo was successfully removed.'
end
def favicon
@appearance.remove_favicon!
@appearance.save
- redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.'
+ redirect_to admin_appearances_path, notice: 'Favicon was successfully removed.'
end
private
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8040a14ef56..8f683ca06ad 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -61,7 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.html do
usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data)
- render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
+ render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
format.json { render json: Gitlab::UsageData.to_json }
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index eeabcc0c9bb..7f4aa8244ac 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -46,6 +46,8 @@ class ApplicationController < ActionController::Base
:git_import_enabled?, :gitlab_project_import_enabled?,
:manifest_import_enabled?
+ DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze
+
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
render "errors/encoding", layout: "errors", status: 500
@@ -244,6 +246,13 @@ class ApplicationController < ActionController::Base
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-UA-Compatible'] = 'IE=edge'
headers['X-Content-Type-Options'] = 'nosniff'
+
+ if current_user
+ # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
+ # concerns due to caching private data.
+ headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL
+ headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility
+ end
end
def validate_user_service_ticket!
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
new file mode 100644
index 00000000000..250f42f3096
--- /dev/null
+++ b/app/controllers/clusters/applications_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class Clusters::ApplicationsController < Clusters::BaseController
+ before_action :cluster
+ before_action :authorize_create_cluster!, only: [:create]
+
+ def create
+ Clusters::Applications::CreateService
+ .new(@cluster, current_user, create_cluster_application_params)
+ .execute(request)
+
+ head :no_content
+ rescue Clusters::Applications::CreateService::InvalidApplicationError
+ render_404
+ rescue StandardError
+ head :bad_request
+ end
+
+ private
+
+ def cluster
+ @cluster ||= clusterable.clusters.find(params[:id]) || render_404
+ end
+
+ def create_cluster_application_params
+ params.permit(:application, :hostname)
+ end
+end
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
new file mode 100644
index 00000000000..ef42f7c4074
--- /dev/null
+++ b/app/controllers/clusters/base_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class Clusters::BaseController < ApplicationController
+ include RoutableActions
+
+ skip_before_action :authenticate_user!
+ before_action :authorize_read_cluster!
+
+ helper_method :clusterable
+
+ private
+
+ def cluster
+ @cluster ||= clusterable.clusters.find(params[:id])
+ .present(current_user: current_user)
+ end
+
+ def authorize_update_cluster!
+ access_denied! unless can?(current_user, :update_cluster, cluster)
+ end
+
+ def authorize_admin_cluster!
+ access_denied! unless can?(current_user, :admin_cluster, cluster)
+ end
+
+ def authorize_read_cluster!
+ access_denied! unless can?(current_user, :read_cluster, clusterable)
+ end
+
+ def authorize_create_cluster!
+ access_denied! unless can?(current_user, :create_cluster, clusterable)
+ end
+
+ def clusterable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
new file mode 100644
index 00000000000..f6f2060ebb5
--- /dev/null
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+class Clusters::ClustersController < Clusters::BaseController
+ include RoutableActions
+
+ before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
+ before_action :generate_gcp_authorize_url, only: [:new]
+ before_action :validate_gcp_token, only: [:new]
+ before_action :gcp_cluster, only: [:new]
+ before_action :user_cluster, only: [:new]
+ before_action :authorize_create_cluster!, only: [:new]
+ before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
+ before_action :update_applications_status, only: [:cluster_status]
+
+ helper_method :token_in_session
+
+ STATUS_POLLING_INTERVAL = 10_000
+
+ def index
+ clusters = ClustersFinder.new(clusterable, current_user, :all).execute
+ @clusters = clusters.page(params[:page]).per(20)
+ end
+
+ def new
+ end
+
+ # Overridding ActionController::Metal#status is NOT a good idea
+ def cluster_status
+ respond_to do |format|
+ format.json do
+ Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
+
+ render json: ClusterSerializer
+ .new(current_user: @current_user)
+ .represent_status(@cluster)
+ end
+ end
+ end
+
+ def show
+ end
+
+ def update
+ Clusters::UpdateService
+ .new(current_user, update_params)
+ .execute(cluster)
+
+ if cluster.valid?
+ respond_to do |format|
+ format.json do
+ head :no_content
+ end
+ format.html do
+ flash[:notice] = _('Kubernetes cluster was successfully updated.')
+ redirect_to cluster.show_path
+ end
+ end
+ else
+ respond_to do |format|
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
+ end
+ end
+
+ def destroy
+ if cluster.destroy
+ flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
+ redirect_to clusterable.index_path, status: :found
+ else
+ flash[:notice] = _('Kubernetes cluster integration was not removed.')
+ render :show
+ end
+ end
+
+ def create_gcp
+ @gcp_cluster = ::Clusters::CreateService
+ .new(current_user, create_gcp_cluster_params)
+ .execute(access_token: token_in_session)
+ .present(current_user: current_user)
+
+ if @gcp_cluster.persisted?
+ redirect_to @gcp_cluster.show_path
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ user_cluster
+
+ render :new, locals: { active_tab: 'gcp' }
+ end
+ end
+
+ def create_user
+ @user_cluster = ::Clusters::CreateService
+ .new(current_user, create_user_cluster_params)
+ .execute(access_token: token_in_session)
+ .present(current_user: current_user)
+
+ if @user_cluster.persisted?
+ redirect_to @user_cluster.show_path
+ else
+ generate_gcp_authorize_url
+ validate_gcp_token
+ gcp_cluster
+
+ render :new, locals: { active_tab: 'user' }
+ end
+ end
+
+ private
+
+ def update_params
+ if cluster.managed?
+ params.require(:cluster).permit(
+ :enabled,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace
+ ]
+ )
+ else
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :api_url,
+ :token,
+ :ca_cert,
+ :namespace
+ ]
+ )
+ end
+ end
+
+ def create_gcp_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type,
+ :legacy_abac
+ ]).merge(
+ provider_type: :gcp,
+ platform_type: :kubernetes,
+ clusterable: clusterable.subject
+ )
+ end
+
+ def create_user_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ platform_kubernetes_attributes: [
+ :namespace,
+ :api_url,
+ :token,
+ :ca_cert,
+ :authorization_type
+ ]).merge(
+ provider_type: :user,
+ platform_type: :kubernetes,
+ clusterable: clusterable.subject
+ )
+ end
+
+ def generate_gcp_authorize_url
+ state = generate_session_key_redirect(clusterable.new_path.to_s)
+
+ @authorize_url = GoogleApi::CloudPlatform::Client.new(
+ nil, callback_google_api_auth_url,
+ state: state).authorize_url
+ rescue GoogleApi::Auth::ConfigMissingError
+ # no-op
+ end
+
+ def gcp_cluster
+ @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_provider_gcp
+ end
+ end
+
+ def user_cluster
+ @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
+ cluster.build_platform_kubernetes
+ end
+ end
+
+ def validate_gcp_token
+ @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ end
+
+ def token_in_session
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ end
+
+ def expires_at_in_session
+ @expires_at_in_session ||=
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
+ end
+
+ def generate_session_key_redirect(uri)
+ GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
+ session[key] = uri
+ end
+ end
+
+ def update_applications_status
+ @cluster.applications.each(&:schedule_status_update)
+ end
+end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index b3777fd2b0f..f644702cbdb 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/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb
new file mode 100644
index 00000000000..f59440dbc59
--- /dev/null
+++ b/app/controllers/concerns/project_unauthorized.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ProjectUnauthorized
+ extend ActiveSupport::Concern
+
+ # EE would override this
+ def project_unauthorized_proc
+ # no-op
+ end
+end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 88939b002b2..5624eb3aa45 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -3,23 +3,25 @@
module RoutableActions
extend ActiveSupport::Concern
- def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
+ def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
- handle_not_found_or_authorized(routable)
+ if not_found_or_authorized_proc
+ not_found_or_authorized_proc.call(routable)
+ end
+
+ route_not_found unless performed?
+
nil
end
end
- # This is overridden in gitlab-ee.
- def handle_not_found_or_authorized(_routable)
- route_not_found
- end
-
def routable_authorized?(routable, extra_authorization_proc)
+ return false unless routable
+
action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable)
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 6e17bc212e4..3802aa5f40f 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects
+ before_action :groups, only: :index
before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
format.html do
- @milestone_states = GlobalMilestone.states_count(@projects)
+ @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id))
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
@@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
@milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
+
+ def groups
+ @groups ||= GroupsFinder.new(current_user, state_all: true).execute
+ end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index a7cee426cf1..b42116b0f36 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
- @milestone_states = GlobalMilestone.states_count(group_projects, group)
+ @milestone_states = Milestone.states_count(group_projects, [group])
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 93f3eb2be6d..c1dcc463de7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_pipeline!
def show
- define_secret_variables
+ define_ci_variables
end
def reset_registration_token
@@ -19,7 +19,7 @@ module Groups
private
- def define_secret_variables
+ def define_ci_variables
@variable = Ci::GroupVariable.new(group: group)
.present(current_user: current_user)
@variables = group.variables.order_key_asc
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 382c684a408..f067ef625aa 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -23,7 +23,7 @@ class Import::GiteaController < Import::GithubController
:"#{provider}_host_url"
end
- # Overriden methods
+ # Overridden methods
def provider
:gitea
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index e3eec5a020d..58565aaf8c9 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -103,7 +103,7 @@ class Import::GithubController < Import::BaseController
{ github_access_token: session[access_token_key] }
end
- # The following methods are overriden in subclasses
+ # The following methods are overridden in subclasses
def provider
:github
end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 894a6a431e3..705389749d8 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -3,7 +3,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
layout 'profile'
- # Overriden from Doorkeeper::AuthorizationsController to
+ # Overridden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
if pre_auth.authorizable?
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index a2bdcaefa9b..e0677ce3fbc 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -3,6 +3,7 @@
class Projects::ApplicationController < ApplicationController
include CookiesHelper
include RoutableActions
+ include ProjectUnauthorized
include ChecksCollaboration
skip_before_action :authenticate_user!
@@ -21,7 +22,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
- @project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
+ @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc)
end
def build_canonical_path(project)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 56a884b8a2a..c02ec407262 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
apply_diff_view_cookie!
@blob.load_all_data!
- @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines
+ @lines = @blob.present.highlight.lines
@form = UnfoldForm.new(params)
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index bcea96bce94..c7b6218d007 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -1,29 +1,17 @@
# frozen_string_literal: true
-class Projects::Clusters::ApplicationsController < Projects::ApplicationController
- before_action :cluster
- before_action :authorize_read_cluster!
- before_action :authorize_create_cluster!, only: [:create]
+class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include ProjectUnauthorized
- def create
- Clusters::Applications::CreateService
- .new(@cluster, current_user, create_cluster_application_params)
- .execute(request)
-
- head :no_content
- rescue Clusters::Applications::CreateService::InvalidApplicationError
- render_404
- rescue StandardError
- head :bad_request
- end
+ prepend_before_action :project
private
- def cluster
- @cluster ||= project.clusters.find(params[:id]) || render_404
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end
- def create_cluster_application_params
- params.permit(:application, :hostname)
+ def project
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 62adc66fb09..feda6deeaa6 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,224 +1,24 @@
# frozen_string_literal: true
-class Projects::ClustersController < Projects::ApplicationController
- before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
- before_action :authorize_read_cluster!
- before_action :generate_gcp_authorize_url, only: [:new]
- before_action :validate_gcp_token, only: [:new]
- before_action :gcp_cluster, only: [:new]
- before_action :user_cluster, only: [:new]
- before_action :authorize_create_cluster!, only: [:new]
- before_action :authorize_update_cluster!, only: [:update]
- before_action :authorize_admin_cluster!, only: [:destroy]
- before_action :update_applications_status, only: [:status]
- helper_method :token_in_session
+class Projects::ClustersController < Clusters::ClustersController
+ include ProjectUnauthorized
- STATUS_POLLING_INTERVAL = 10_000
+ prepend_before_action :project
+ before_action :repository
- def index
- clusters = ClustersFinder.new(project, current_user, :all).execute
- @clusters = clusters.page(params[:page]).per(20)
- end
-
- def new
- end
-
- def status
- respond_to do |format|
- format.json do
- Gitlab::PollingInterval.set_header(response, interval: STATUS_POLLING_INTERVAL)
-
- render json: ClusterSerializer
- .new(project: @project, current_user: @current_user)
- .represent_status(@cluster)
- end
- end
- end
-
- def show
- end
-
- def update
- Clusters::UpdateService
- .new(current_user, update_params)
- .execute(cluster)
-
- if cluster.valid?
- respond_to do |format|
- format.json do
- head :no_content
- end
- format.html do
- flash[:notice] = _('Kubernetes cluster was successfully updated.')
- redirect_to project_cluster_path(project, cluster)
- end
- end
- else
- respond_to do |format|
- format.json { head :bad_request }
- format.html { render :show }
- end
- end
- end
-
- def destroy
- if cluster.destroy
- flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to project_clusters_path(project), status: :found
- else
- flash[:notice] = _('Kubernetes cluster integration was not removed.')
- render :show
- end
- end
-
- def create_gcp
- @gcp_cluster = ::Clusters::CreateService
- .new(current_user, create_gcp_cluster_params)
- .execute(project: project, access_token: token_in_session)
-
- if @gcp_cluster.persisted?
- redirect_to project_cluster_path(project, @gcp_cluster)
- else
- generate_gcp_authorize_url
- validate_gcp_token
- user_cluster
-
- render :new, locals: { active_tab: 'gcp' }
- end
- end
-
- def create_user
- @user_cluster = ::Clusters::CreateService
- .new(current_user, create_user_cluster_params)
- .execute(project: project, access_token: token_in_session)
-
- if @user_cluster.persisted?
- redirect_to project_cluster_path(project, @user_cluster)
- else
- generate_gcp_authorize_url
- validate_gcp_token
- gcp_cluster
-
- render :new, locals: { active_tab: 'user' }
- end
- end
+ layout 'project'
private
- def cluster
- @cluster ||= project.clusters.find(params[:id])
- .present(current_user: current_user)
- end
-
- def update_params
- if cluster.managed?
- params.require(:cluster).permit(
- :enabled,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace
- ]
- )
- else
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :api_url,
- :token,
- :ca_cert,
- :namespace
- ]
- )
- end
- end
-
- def create_gcp_cluster_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type,
- :legacy_abac
- ]).merge(
- provider_type: :gcp,
- platform_type: :kubernetes
- )
- end
-
- def create_user_cluster_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :environment_scope,
- platform_kubernetes_attributes: [
- :namespace,
- :api_url,
- :token,
- :ca_cert,
- :authorization_type
- ]).merge(
- provider_type: :user,
- platform_type: :kubernetes
- )
- end
-
- def generate_gcp_authorize_url
- state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
-
- @authorize_url = GoogleApi::CloudPlatform::Client.new(
- nil, callback_google_api_auth_url,
- state: state).authorize_url
- rescue GoogleApi::Auth::ConfigMissingError
- # no-op
- end
-
- def gcp_cluster
- @gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_provider_gcp
- end
- end
-
- def user_cluster
- @user_cluster = ::Clusters::Cluster.new.tap do |cluster|
- cluster.build_platform_kubernetes
- end
- end
-
- def validate_gcp_token
- @valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- end
-
- def token_in_session
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
- end
-
- def expires_at_in_session
- @expires_at_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
- end
-
- def generate_session_key_redirect(uri)
- GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
- session[key] = uri
- end
- end
-
- def authorize_update_cluster!
- access_denied! unless can?(current_user, :update_cluster, cluster)
+ def clusterable
+ @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
end
- def authorize_admin_cluster!
- access_denied! unless can?(current_user, :admin_cluster, cluster)
+ def project
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
end
- def update_applications_status
- @cluster.applications.each(&:schedule_status_update)
+ def repository
+ @repository ||= project.repository
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 00b63f55710..32fc5140366 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -43,7 +43,7 @@ class Projects::CommitController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
- @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref]
+ @pipelines = @pipelines.where(ref: params[:ref]).page(params[:page]).per(30) if params[:ref]
respond_to do |format|
format.html
@@ -53,6 +53,7 @@ class Projects::CommitController < Projects::ApplicationController
render json: {
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines.count
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index be708835e30..c0aa39d87c6 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
+ rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :unprocessable_entity
end
+ def render_503(exception)
+ render plain: exception.message, status: :service_unavailable
+ end
+
def access
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b06a6f3bb0d..308f666394c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -9,12 +9,25 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuesCalendar
include SpammableActions
- prepend_before_action :authenticate_user!, only: [:new]
+ def self.authenticate_user_only_actions
+ %i[new]
+ end
+
+ def self.issue_except_actions
+ %i[index calendar new create bulk_update]
+ end
+
+ def self.set_issuables_index_only_actions
+ %i[index calendar]
+ end
+
+ prepend_before_action :authenticate_user!, only: authenticate_user_only_actions
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
- before_action :set_issuables_index, only: [:index, :calendar]
+ before_action :issue, except: issue_except_actions
+
+ before_action :set_issuables_index, only: set_issuables_index_only_actions
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 5639402a1e9..bbf662a63c8 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -89,6 +89,8 @@ 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/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 757b03d0b0e..4bdb857b2d9 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -84,13 +84,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def pipelines
- @pipelines = @merge_request.all_pipelines
+ @pipelines = @merge_request.all_pipelines.page(params[:page]).per(30)
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: {
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines.count
@@ -168,7 +169,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def merge
- return access_denied! unless @merge_request.can_be_merged_by?(current_user)
+ access_check_result = merge_access_check
+
+ return access_check_result if access_check_result
status = merge!
@@ -201,9 +204,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def ci_environments_status
- environments = @merge_request.environments_for(current_user).map do |environment|
- EnvironmentStatus.new(environment, @merge_request)
- end
+ environments = if ci_environments_status_on_merge_result?
+ EnvironmentStatus.after_merge_request(@merge_request, current_user)
+ else
+ EnvironmentStatus.for_merge_request(@merge_request, current_user)
+ end
render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments)
end
@@ -241,6 +246,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
+ def ci_environments_status_on_merge_result?
+ params[:environment_target] == 'merge_commit'
+ end
+
def target_branch_missing?
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
@@ -256,6 +265,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :failed
end
+ merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params)
+
+ unless merge_service.hooks_validation_pass?(@merge_request)
+ return :hook_validation_error
+ end
+
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
@@ -318,6 +333,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
access_denied! unless access_check
end
+ def merge_access_check
+ access_denied! unless @merge_request.can_be_merged_by?(current_user)
+ end
+
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 3a1344651df..75e590f3f33 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -68,7 +68,7 @@ module Projects
def define_variables
define_runners_variables
- define_secret_variables
+ define_ci_variables
define_triggers_variables
define_badges_variables
define_auto_devops_variables
@@ -90,7 +90,7 @@ module Projects
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end
- def define_secret_variables
+ def define_ci_variables
@variable = ::Ci::Variable.new(project: project)
.present(current_user: current_user)
@variables = project.variables.order_key_asc
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ee438e160f2..7f4a9f5151b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -276,7 +276,7 @@ class ProjectsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
# Render project landing depending of which features are available
- # So if page is not availble in the list it renders the next page
+ # So if page is not available in the list it renders the next page
#
# pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
index e2283f3266e..45955783be9 100644
--- a/app/finders/autocomplete/users_finder.rb
+++ b/app/finders/autocomplete/users_finder.rb
@@ -72,7 +72,6 @@ module Autocomplete
author_id.present? && current_user
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_users
if project
project.authorized_users.union_with_user(author_id)
@@ -84,6 +83,5 @@ module Autocomplete
User.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
index b40d6c41b71..0cce493b73e 100644
--- a/app/finders/clusters_finder.rb
+++ b/app/finders/clusters_finder.rb
@@ -1,20 +1,20 @@
# frozen_string_literal: true
class ClustersFinder
- def initialize(project, user, scope)
- @project = project
+ def initialize(clusterable, user, scope)
+ @clusterable = clusterable
@user = user
@scope = scope || :active
end
def execute
- clusters = project.clusters
+ clusters = clusterable.clusters
filter_by_scope(clusters)
end
private
- attr_reader :project, :user, :scope
+ attr_reader :clusterable, :user, :scope
def filter_by_scope(clusters)
case scope.to_sym
diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb
index e038636f0c4..220f62bcc7f 100644
--- a/app/finders/concerns/finder_with_cross_project_access.rb
+++ b/app/finders/concerns/finder_with_cross_project_access.rb
@@ -16,7 +16,6 @@ module FinderWithCrossProjectAccess
end
override :execute
- # rubocop: disable CodeReuse/ActiveRecord
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
@@ -30,7 +29,6 @@ module FinderWithCrossProjectAccess
original
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 9d57d2d3bc9..c96979619fd 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -131,7 +131,6 @@ class GroupDescendantsFinder
.with_selects_for_list(archived: params[:archived])
end
- # rubocop: disable CodeReuse/ActiveRecord
def subgroups
return Group.none unless Group.supports_nested_groups?
@@ -145,7 +144,6 @@ class GroupDescendantsFinder
groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/Finder
def direct_child_projects
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index a35a3ed6142..ea954f98220 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -40,7 +40,6 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params
- # rubocop: disable CodeReuse/ActiveRecord
def all_groups
return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
@@ -52,7 +51,6 @@ class GroupsFinder < UnionFinder
groups << Group.none if groups.empty?
groups
end
- # rubocop: enable CodeReuse/ActiveRecord
def groups_for_ancestors
current_user.authorized_groups
@@ -82,11 +80,9 @@ class GroupsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def owned_groups
current_user&.owned_groups || Group.none
end
- # rubocop: enable CodeReuse/ActiveRecord
def include_public_groups?
current_user.nil? || all_available?
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index eb3d2498830..93bef592c65 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -192,11 +192,6 @@ class IssuableFinder
params[:milestone_title].present?
end
- def filter_by_no_milestone?
- milestones? && params[:milestone_title] == Milestone::None.title
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
def milestones
return @milestones if defined?(@milestones)
@@ -217,7 +212,6 @@ class IssuableFinder
Milestone.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def labels?
params[:label_name].present?
@@ -227,7 +221,6 @@ class IssuableFinder
labels? && params[:label_name].include?(Label::None.title)
end
- # rubocop: disable CodeReuse/ActiveRecord
def labels
return @labels if defined?(@labels)
@@ -238,7 +231,6 @@ class IssuableFinder
Label.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def assignee_id?
params[:assignee_id].present?
@@ -248,15 +240,6 @@ class IssuableFinder
params[:assignee_username].present?
end
- def filter_by_no_assignee?
- # Assignee_id takes precedence over assignee_username
- [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
- end
-
- def filter_by_any_assignee?
- params[:assignee_id].to_s.downcase == FILTER_ANY
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def assignee
return @assignee if defined?(@assignee)
@@ -422,6 +405,15 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def filter_by_no_assignee?
+ # Assignee_id takes precedence over assignee_username
+ [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
+ end
+
+ def filter_by_any_assignee?
+ params[:assignee_id].to_s.downcase == FILTER_ANY
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_author(items)
if author
@@ -436,18 +428,6 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def filter_by_upcoming_milestone?
- params[:milestone_title] == Milestone::Upcoming.name
- end
-
- def filter_by_any_milestone?
- params[:milestone_title] == Milestone::Any.title
- end
-
- def filter_by_started_milestone?
- params[:milestone_title] == Milestone::Started.name
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
@@ -469,6 +449,24 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def filter_by_no_milestone?
+ # Accepts `No Milestone` for compatibility
+ params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
+ end
+
+ def filter_by_any_milestone?
+ # Accepts `Any Milestone` for compatibility
+ params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title
+ end
+
+ def filter_by_upcoming_milestone?
+ params[:milestone_title] == Milestone::Upcoming.name
+ end
+
+ def filter_by_started_milestone?
+ params[:milestone_title] == Milestone::Started.name
+ end
+
def by_label(items)
return items unless labels?
@@ -484,12 +482,27 @@ class IssuableFinder
def by_my_reaction_emoji(items)
if params[:my_reaction_emoji].present? && current_user
- items = items.awarded(current_user, params[:my_reaction_emoji])
+ items =
+ if filter_by_no_reaction?
+ items.not_awarded(current_user)
+ elsif filter_by_any_reaction?
+ items.awarded(current_user)
+ else
+ items.awarded(current_user, params[:my_reaction_emoji])
+ end
end
items
end
+ def filter_by_no_reaction?
+ params[:my_reaction_emoji].to_s.downcase == FILTER_NONE
+ end
+
+ def filter_by_any_reaction?
+ params[:my_reaction_emoji].to_s.downcase == FILTER_ANY
+ end
+
def label_names
if labels?
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index cee57a83df4..45e494725d7 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -135,7 +135,6 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
- # rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
@@ -149,5 +148,4 @@ class IssuesFinder < IssuableFinder
items
end
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index d000af21be3..e523942ea4c 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -12,7 +12,6 @@ class LabelsFinder < UnionFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none
@@ -21,7 +20,6 @@ class LabelsFinder < UnionFinder
items = by_search(items)
sort(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index 47231ea80c7..9c477978f60 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -20,7 +20,6 @@ class MilestonesFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute
return Milestone.none if project_ids.empty? && group_ids.empty?
@@ -31,7 +30,6 @@ class MilestonesFinder
order(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 5beea92689f..81fd3b7a547 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -3,7 +3,7 @@
class PersonalAccessTokensFinder
attr_accessor :params
- delegate :build, :find, :find_by, to: :execute
+ delegate :build, :find, :find_by, :find_by_token, to: :execute
def initialize(params = {})
@params = params
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 3d0d3219a94..35d0e1acce5 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -12,7 +12,6 @@ class PipelinesFinder
@params = params
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute
unless Ability.allowed?(current_user, :read_pipeline, project)
return Ci::Pipeline.none
@@ -28,7 +27,6 @@ class PipelinesFinder
items = by_yaml_errors(items)
sort_items(items)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 6ececcd4152..93d3c991846 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -88,7 +88,6 @@ class ProjectsFinder < UnionFinder
# rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user.
- # rubocop: disable CodeReuse/ActiveRecord
def collection_without_user
if private_only? || owned_projects? || min_access_level?
Project.none
@@ -96,7 +95,6 @@ class ProjectsFinder < UnionFinder
Project.public_to_user
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def owned_projects?
params[:owned].present?
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 3528e4228b2..f90971bb9f6 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -43,7 +43,6 @@ class SnippetsFinder < UnionFinder
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def authorized_snippets_from_project
if can?(current_user, :read_project_snippet, project)
if project.team.member?(current_user)
@@ -55,7 +54,6 @@ class SnippetsFinder < UnionFinder
Snippet.none
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def authorized_snippets
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index ff7f1e3a9aa..638744a1426 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
module BlobHelper
- def highlight(blob_name, blob_content, repository: nil, plain: false)
- plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE
- highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository)
+ def highlight(file_name, file_content, language: nil, plain: false)
+ highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language)
raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 19eb763e1de..916dcb1a308 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module ClustersHelper
- def has_multiple_clusters?(project)
+ # EE overrides this
+ def has_multiple_clusters?
false
end
@@ -10,7 +11,7 @@ module ClustersHelper
return unless show_gcp_signup_offer?
content_tag :section, class: 'no-animate expanded' do
- render 'projects/clusters/gcp_signup_offer_banner'
+ render 'clusters/clusters/gcp_signup_offer_banner'
end
end
end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 9ece8b0bc5b..57e397f6ca0 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/icons_helper.rb b/app/helpers/icons_helper.rb
index 037004327b9..910c9e9446f 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -153,6 +153,6 @@ module IconsHelper
private
def known_sprites
- @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab-org/gitlab-svgs/dist/icons.json')))['icons']
+ @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons']
end
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 76ed8efe2c6..39f661b5f0c 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -143,7 +143,7 @@ module LabelsHelper
def labels_filter_path(options = {})
project = @target_project || @project
- format = options.delete(:format) || :html
+ format = options.delete(:format)
if project
project_labels_path(project, format, options)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 23d7aa427bb..8f549bfce73 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/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index b33c074d1af..5038dcf9746 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -10,7 +10,7 @@ module PageLayoutHelper
@breadcrumb_title = @page_title.last
end
- # Segments are seperated by middot
+ # Segments are separated by middot
@page_title.join(" · ")
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 602e5afe26b..93b51fb1774 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -45,6 +45,20 @@ module Emails
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
+ def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil)
+ setup_issue_mail(issue_id, recipient_id)
+
+ @milestone = milestone
+ @milestone_url = milestone_url(@milestone)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index be085496731..6524d0c2087 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -40,6 +40,20 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def removed_milestone_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
+ def changed_milestone_merge_request_email(recipient_id, merge_request_id, milestone, updated_by_user_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @milestone = milestone
+ @milestone_url = milestone_url(@milestone)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 2f5b5483e9d..e7e8d96eca4 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -68,6 +68,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message
end
+ def removed_milestone_issue_email
+ Notify.removed_milestone_issue_email(user.id, issue.id, user.id)
+ end
+
+ def changed_milestone_issue_email
+ Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id)
+ end
+
def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end
@@ -80,6 +88,14 @@ class NotifyPreview < ActionMailer::Preview
Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message
end
+ def removed_milestone_merge_request_email
+ Notify.removed_milestone_merge_request_email(user.id, merge_request.id, user.id)
+ end
+
+ def changed_milestone_merge_request_email
+ Notify.changed_milestone_merge_request_email(user.id, merge_request.id, milestone, user.id)
+ end
+
def member_access_denied_email
Notify.member_access_denied_email('project', project.id, user.id).message
end
@@ -143,6 +159,10 @@ class NotifyPreview < ActionMailer::Preview
@merge_request ||= project.merge_requests.first
end
+ def milestone
+ @milestone ||= issue.milestone
+ end
+
def pipeline
@pipeline = Ci::Pipeline.last
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 31a839274b5..4f310e70f4f 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
+# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
+ include Presentable
+ include BlobLanguageFromGitAttributes
+
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
- MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
-
# Finding a viewer for a blob happens based only on extension and whether the
# blob is binary or text, which means 1 blob should only be matched by 1 viewer,
# and the order of these viewers doesn't really matter.
@@ -121,10 +122,6 @@ class Blob < SimpleDelegator
end
end
- def no_highlighting?
- raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE
- end
-
def empty?
raw_size == 0
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index cdfe8175a42..bb5d52fc78d 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -259,8 +259,7 @@ module Ci
end
def schedulable?
- Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) &&
- self.when == 'delayed' && options[:start_in].present?
+ self.when == 'delayed' && options[:start_in].present?
end
def options_scheduled_at
@@ -593,11 +592,11 @@ module Ci
def secret_group_variables
return [] unless project.group
- project.group.secret_variables_for(ref, project)
+ project.group.ci_variables_for(ref, project)
end
def secret_project_variables(environment: persisted_environment)
- project.secret_variables_for(ref: ref, environment: environment)
+ project.ci_variables_for(ref: ref, environment: environment)
end
def steps
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 95efecfc41d..2bd373e0950 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -19,6 +19,13 @@ module Clusters
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
+ has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
+
+ has_many :cluster_groups, class_name: 'Clusters::Group'
+ has_many :groups, through: :cluster_groups, class_name: '::Group'
+
+ has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group'
+ has_one :group, through: :cluster_group, class_name: '::Group'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
@@ -38,8 +45,12 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
+ validates :cluster_type, presence: true
validate :restrict_modification, on: :update
+ validate :no_groups, unless: :group_type?
+ validate :no_projects, unless: :project_type?
+
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
@@ -50,6 +61,12 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
+ enum cluster_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
enum platform_type: {
kubernetes: 1
}
@@ -112,6 +129,13 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes?
end
+ def find_or_initialize_kubernetes_namespace(cluster_project)
+ kubernetes_namespaces.find_or_initialize_by(
+ project: cluster_project.project,
+ cluster_project: cluster_project
+ )
+ end
+
private
def restrict_modification
@@ -122,5 +146,17 @@ module Clusters
true
end
+
+ def no_groups
+ if groups.any?
+ errors.add(:cluster, 'cannot have groups assigned')
+ end
+ end
+
+ def no_projects
+ if projects.any?
+ errors.add(:cluster, 'cannot have projects assigned')
+ end
+ end
end
end
diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb
new file mode 100644
index 00000000000..2b08a9e47f0
--- /dev/null
+++ b/app/models/clusters/group.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Clusters
+ class Group < ActiveRecord::Base
+ self.table_name = 'cluster_groups'
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster'
+ belongs_to :group, class_name: '::Group'
+ end
+end
diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb
index fb5f6b65d9d..ac7f9193b87 100644
--- a/app/models/clusters/kubernetes_namespace.rb
+++ b/app/models/clusters/kubernetes_namespace.rb
@@ -2,6 +2,8 @@
module Clusters
class KubernetesNamespace < ActiveRecord::Base
+ include Gitlab::Kubernetes
+
self.table_name = 'clusters_kubernetes_namespaces'
belongs_to :cluster_project, class_name: 'Clusters::Project'
@@ -12,7 +14,8 @@ module Clusters
validates :namespace, presence: true
validates :namespace, uniqueness: { scope: :cluster_id }
- before_validation :set_namespace_and_service_account_to_default, on: :create
+ delegate :ca_pem, to: :platform_kubernetes, allow_nil: true
+ delegate :api_url, to: :platform_kubernetes, allow_nil: true
attr_encrypted :service_account_token,
mode: :per_attribute_iv,
@@ -23,14 +26,26 @@ module Clusters
"#{namespace}-token"
end
- private
+ def configure_predefined_credentials
+ self.namespace = kubernetes_or_project_namespace
+ self.service_account_name = default_service_account_name
+ end
+
+ def predefined_variables
+ config = YAML.dump(kubeconfig)
- def set_namespace_and_service_account_to_default
- self.namespace ||= default_namespace
- self.service_account_name ||= default_service_account_name
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables
+ .append(key: 'KUBE_SERVICE_ACCOUNT', value: service_account_name)
+ .append(key: 'KUBE_NAMESPACE', value: namespace)
+ .append(key: 'KUBE_TOKEN', value: service_account_token, public: false)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ end
end
- def default_namespace
+ private
+
+ def kubernetes_or_project_namespace
platform_kubernetes&.namespace.presence || project_namespace
end
@@ -45,5 +60,13 @@ module Clusters
def project_slug
"#{project.path}-#{project.id}".downcase
end
+
+ def kubeconfig
+ to_kubeconfig(
+ url: api_url,
+ namespace: namespace,
+ token: service_account_token,
+ ca_pem: ca_pem)
+ end
end
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index f0f791742f4..008e08d9914 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -6,6 +6,7 @@ module Clusters
include Gitlab::Kubernetes
include ReactiveCaching
include EnumWithNil
+ include AfterCommitQueue
RESERVED_NAMESPACES = %w(gitlab-managed-apps).freeze
@@ -43,6 +44,7 @@ module Clusters
validate :prevent_modification, on: :update
after_save :clear_reactive_cache!
+ after_update :update_kubernetes_namespace
alias_attribute :ca_pem, :ca_cert
@@ -67,21 +69,31 @@ module Clusters
end
end
- def predefined_variables
- config = YAML.dump(kubeconfig)
-
+ def predefined_variables(project:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables
- .append(key: 'KUBE_URL', value: api_url)
- .append(key: 'KUBE_TOKEN', value: token, public: false)
- .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
- .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ variables.append(key: 'KUBE_URL', value: api_url)
if ca_pem.present?
variables
.append(key: 'KUBE_CA_PEM', value: ca_pem)
.append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
end
+
+ if kubernetes_namespace = cluster.kubernetes_namespaces.find_by(project: project)
+ variables.concat(kubernetes_namespace.predefined_variables)
+ else
+ # From 11.5, every Clusters::Project should have at least one
+ # Clusters::KubernetesNamespace, so once migration has been completed,
+ # this 'else' branch will be removed. For more information, please see
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433
+ config = YAML.dump(kubeconfig)
+
+ variables
+ .append(key: 'KUBE_URL', value: api_url)
+ .append(key: 'KUBE_TOKEN', value: token, public: false)
+ .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+ end
end
end
@@ -199,6 +211,14 @@ module Clusters
true
end
+
+ def update_kubernetes_namespace
+ return unless namespace_changed?
+
+ run_after_commit do
+ ClusterPlatformConfigureWorker.perform_async(cluster_id)
+ end
+ end
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 344f091c872..95c88e11a6e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4,
missing_dependency_failure: 5,
runner_unsupported: 6,
- stale_schedule: 7
+ stale_schedule: 7,
+ job_execution_timeout: 8
}
##
@@ -166,12 +167,12 @@ class CommitStatus < ActiveRecord::Base
false
end
- # To be overriden when inherrited from
+ # To be overridden when inherrited from
def retryable?
false
end
- # To be overriden when inherrited from
+ # To be overridden when inherrited from
def cancelable?
false
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 6f29c92d176..60b7ec2815c 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -13,13 +13,13 @@ module Awardable
end
class_methods do
- def awarded(user, name)
+ def awarded(user, name = nil)
sql = <<~EOL
EXISTS (
SELECT TRUE
FROM award_emoji
WHERE user_id = :user_id AND
- name = :name AND
+ #{"name = :name AND" if name.present?}
awardable_type = :awardable_type AND
awardable_id = #{self.arel_table.name}.id
)
@@ -28,6 +28,20 @@ module Awardable
where(sql, user_id: user.id, name: name, awardable_type: self.name)
end
+ def not_awarded(user)
+ sql = <<~EOL
+ NOT EXISTS (
+ SELECT TRUE
+ FROM award_emoji
+ WHERE user_id = :user_id AND
+ awardable_type = :awardable_type AND
+ awardable_id = #{self.arel_table.name}.id
+ )
+ EOL
+
+ where(sql, user_id: user.id, awardable_type: self.name)
+ end
+
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
diff --git a/app/models/concerns/blob_language_from_git_attributes.rb b/app/models/concerns/blob_language_from_git_attributes.rb
new file mode 100644
index 00000000000..70213d22147
--- /dev/null
+++ b/app/models/concerns/blob_language_from_git_attributes.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Applicable for blob classes with project attribute
+module BlobLanguageFromGitAttributes
+ extend ActiveSupport::Concern
+
+ def language_from_gitattributes
+ return nil unless project
+
+ repository = project.repository
+ repository.gitattribute(path, 'gitlab-language')
+ end
+end
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index f8034be8376..75592bb63e2 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -12,12 +12,12 @@ module CacheableAttributes
"#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
end
- # Can be overriden
+ # Can be overridden
def current_without_cache
last
end
- # Can be overriden
+ # Can be overridden
def defaults
{}
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 91052013592..e57a3383544 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -42,6 +42,7 @@ module DeploymentPlatform
{
name: 'kubernetes-template',
projects: [self],
+ cluster_type: :project_type,
provider_type: :user,
platform_type: :kubernetes,
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index c342d01243e..2bfa7da6c1c 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -7,7 +7,7 @@
# `delete_all` is efficient as it deletes all rows with a single `DELETE` query.
#
# It's better to use `delete_all` as our best practice, however,
-# if external data (e.g. ObjectStorage, FileStorage or Redis) are assosiated with database records,
+# if external data (e.g. ObjectStorage, FileStorage or Redis) are associated with database records,
# it is difficult to accomplish it.
#
# This module defines a format to use `delete_all` and delete associated external data.
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 2aa52bbaeea..69c5affe142 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -9,6 +9,7 @@
module Issuable
extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
+ include Redactable
include CacheMarkdownField
include Participable
include Mentionable
@@ -32,6 +33,8 @@ module Issuable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
+ redact_field :description
+
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
@@ -360,7 +363,7 @@ module Issuable
end
##
- # Overriden in MergeRequest
+ # Overridden in MergeRequest
#
def wipless_title_changed(old_title)
old_title != title
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 098eed137ba..eb315058c3a 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -23,7 +23,7 @@ module Noteable
end
def supports_discussions?
- DiscussionNote::NOTEABLE_TYPES.include?(base_class_name)
+ DiscussionNote.noteable_types.include?(base_class_name)
end
def discussions_rendered_on_frontend?
diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb
new file mode 100644
index 00000000000..5ad96d6cc46
--- /dev/null
+++ b/app/models/concerns/redactable.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# This module searches and redacts sensitive information in
+# redactable fields. Currently only unsubscribe link is redacted.
+# Add following lines into your model:
+#
+# include Redactable
+# redact_field :foo
+#
+module Redactable
+ extend ActiveSupport::Concern
+
+ UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}
+
+ class_methods do
+ def redact_field(field)
+ before_validation do
+ redact_field!(field) if attribute_changed?(field)
+ end
+ end
+ end
+
+ private
+
+ def redact_field!(field)
+ text = public_send(field) # rubocop:disable GitlabSecurity/PublicSend
+ return unless text.present?
+
+ redacted = text.gsub(UNSUBSCRIBE_PATTERN, '/sent_notifications/REDACTED/unsubscribe')
+
+ public_send("#{field}=", redacted) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 7723c07279d..af699eeebce 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -94,7 +94,7 @@ module Storage
if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
- # Remove namespace directroy async with delay so
+ # Remove namespace directory async with delay so
# GitLab has time to remove all projects first
run_after_commit do
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 522b65e4205..23a43aec677 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -5,57 +5,53 @@ module TokenAuthenticatable
private
- def write_new_token(token_field)
- new_token = generate_available_token(token_field)
- write_attribute(token_field, new_token)
- end
-
- def generate_available_token(token_field)
- loop do
- token = generate_token(token_field)
- break token unless self.class.unscoped.find_by(token_field => token)
- end
- end
-
- def generate_token(token_field)
- Devise.friendly_token
- end
-
class_methods do
- def authentication_token_fields
- @token_fields || []
- end
-
private # rubocop:disable Lint/UselessAccessModifier
- def add_authentication_token_field(token_field)
+ def add_authentication_token_field(token_field, options = {})
@token_fields = [] unless @token_fields
+ unique = options.fetch(:unique, true)
+
+ if @token_fields.include?(token_field)
+ raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
+ end
+
@token_fields << token_field
- define_singleton_method("find_by_#{token_field}") do |token|
- find_by(token_field => token) if token
+ attr_accessor :cleartext_tokens
+
+ strategy = if options[:digest]
+ TokenAuthenticatableStrategies::Digest.new(self, token_field, options)
+ else
+ TokenAuthenticatableStrategies::Insecure.new(self, token_field, options)
+ end
+
+ if unique
+ define_singleton_method("find_by_#{token_field}") do |token|
+ strategy.find_token_authenticatable(token)
+ end
end
- define_method("ensure_#{token_field}") do
- current_token = read_attribute(token_field)
- current_token.blank? ? write_new_token(token_field) : current_token
+ define_method(token_field) do
+ strategy.get_token(self)
end
define_method("set_#{token_field}") do |token|
- write_attribute(token_field, token) if token
+ strategy.set_token(self, token)
+ end
+
+ define_method("ensure_#{token_field}") do
+ strategy.ensure_token(self)
end
# Returns a token, but only saves when the database is in read & write mode
define_method("ensure_#{token_field}!") do
- send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
-
- read_attribute(token_field)
+ strategy.ensure_token!(self)
end
# Resets the token, but only saves when the database is in read & write mode
define_method("reset_#{token_field}!") do
- write_new_token(token_field)
- save! if Gitlab::Database.read_write?
+ strategy.reset_token!(self)
end
end
end
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
new file mode 100644
index 00000000000..413721d3e6c
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Base
+ def initialize(klass, token_field, options)
+ @klass = klass
+ @token_field = token_field
+ @options = options
+ end
+
+ def find_token_authenticatable(instance, unscoped = false)
+ raise NotImplementedError
+ end
+
+ def get_token(instance)
+ raise NotImplementedError
+ end
+
+ def set_token(instance)
+ raise NotImplementedError
+ end
+
+ def ensure_token(instance)
+ write_new_token(instance) unless token_set?(instance)
+ end
+
+ # Returns a token, but only saves when the database is in read & write mode
+ def ensure_token!(instance)
+ reset_token!(instance) unless token_set?(instance)
+ get_token(instance)
+ end
+
+ # Resets the token, but only saves when the database is in read & write mode
+ def reset_token!(instance)
+ write_new_token(instance)
+ instance.save! if Gitlab::Database.read_write?
+ end
+
+ protected
+
+ def write_new_token(instance)
+ new_token = generate_available_token
+ set_token(instance, new_token)
+ end
+
+ def unique
+ @options.fetch(:unique, true)
+ end
+
+ def generate_available_token
+ loop do
+ token = generate_token
+ break token unless unique && find_token_authenticatable(token, true)
+ end
+ end
+
+ def generate_token
+ @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token
+ end
+
+ def relation(unscoped)
+ unscoped ? @klass.unscoped : @klass
+ end
+
+ def token_set?(instance)
+ raise NotImplementedError
+ end
+
+ def token_field_name
+ @token_field
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb
new file mode 100644
index 00000000000..9926662ed66
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/digest.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Digest < Base
+ def find_token_authenticatable(token, unscoped = false)
+ return unless token
+
+ token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token))
+
+ if @options[:fallback]
+ token_authenticatable ||= fallback_strategy.find_token_authenticatable(token)
+ end
+
+ token_authenticatable
+ end
+
+ def get_token(instance)
+ token = instance.cleartext_tokens&.[](@token_field)
+ token ||= fallback_strategy.get_token(instance) if @options[:fallback]
+
+ token
+ end
+
+ def set_token(instance, token)
+ return unless token
+
+ instance.cleartext_tokens ||= {}
+ instance.cleartext_tokens[@token_field] = token
+ instance[token_field_name] = Gitlab::CryptoHelper.sha256(token)
+ instance[@token_field] = nil if @options[:fallback]
+ end
+
+ protected
+
+ def fallback_strategy
+ @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options)
+ end
+
+ def token_set?(instance)
+ token_digest = instance.read_attribute(token_field_name)
+ token_digest ||= instance.read_attribute(@token_field) if @options[:fallback]
+
+ token_digest.present?
+ end
+
+ def token_field_name
+ "#{@token_field}_digest"
+ end
+ end
+end
diff --git a/app/models/concerns/token_authenticatable_strategies/insecure.rb b/app/models/concerns/token_authenticatable_strategies/insecure.rb
new file mode 100644
index 00000000000..5f915259521
--- /dev/null
+++ b/app/models/concerns/token_authenticatable_strategies/insecure.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module TokenAuthenticatableStrategies
+ class Insecure < Base
+ def find_token_authenticatable(token, unscoped = false)
+ relation(unscoped).find_by(@token_field => token) if token
+ end
+
+ def get_token(instance)
+ instance.read_attribute(@token_field)
+ end
+
+ def set_token(instance, token)
+ instance[@token_field] = token if token
+ end
+
+ protected
+
+ def token_set?(instance)
+ instance.read_attribute(@token_field).present?
+ end
+ end
+end
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
index e231af5368d..2bdef2a40e4 100644
--- a/app/models/concerns/with_uploads.rb
+++ b/app/models/concerns/with_uploads.rb
@@ -2,7 +2,7 @@
# Mounted uploaders are destroyed by carrierwave's after_commit
# hook. This hook fetches upload location (local vs remote) from
-# Upload model. So it's neccessary to make sure that during that
+# Upload model. So it's necessary to make sure that during that
# after_commit hook model's associated uploads are not deleted yet.
# IOW we can not use dependent: :destroy :
# has_many :uploads, as: :model, dependent: :destroy
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 62dc0f2cbeb..ee5b96e7454 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base
metrics&.merge(deployment_time: created_at.to_i) || {}
end
+ def status
+ 'success'
+ end
+
private
def prometheus_adapter
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 95694377fe3..5f59e4832db 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -8,12 +8,14 @@ class DiffNote < Note
include DiffPositionableNote
include Gitlab::Utils::StrongMemoize
- NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
+ def self.noteable_types
+ %w(MergeRequest Commit)
+ end
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
- validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
+ validates :noteable_type, inclusion: { in: noteable_types }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index 89d86aaed66..142cbdcdfa6 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -5,9 +5,11 @@
# A note of this type can be resolvable.
class DiscussionNote < Note
# Names of all implementers of `Noteable` that support discussions.
- NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze
+ def self.noteable_types
+ %w(MergeRequest Issue Commit Snippet)
+ end
- validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
+ validates :noteable_type, inclusion: { in: noteable_types }
def discussion_class(*)
Discussion
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 5ff3acc0e58..a84871f7253 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -3,21 +3,33 @@
class EnvironmentStatus
include Gitlab::Utils::StrongMemoize
- attr_reader :environment, :merge_request
+ attr_reader :environment, :merge_request, :sha
delegate :id, to: :environment
delegate :name, to: :environment
delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true
+ delegate :status, to: :deployment
- def initialize(environment, merge_request)
+ def self.for_merge_request(mr, user)
+ build_environments_status(mr, user, mr.head_pipeline)
+ end
+
+ def self.after_merge_request(mr, user)
+ return [] unless mr.merged?
+
+ build_environments_status(mr, user, mr.merge_pipeline)
+ end
+
+ def initialize(environment, merge_request, sha)
@environment = environment
@merge_request = merge_request
+ @sha = sha
end
def deployment
strong_memoize(:deployment) do
- environment.first_deployment_for(merge_request.diff_head_sha)
+ environment.first_deployment_for(sha)
end
end
@@ -26,10 +38,9 @@ class EnvironmentStatus
end
def changes
- sha = merge_request.diff_head_sha
return [] if project.route_map_for(sha).nil?
- changed_files.map { |file| build_change(file, sha) }.compact
+ changed_files.map { |file| build_change(file) }.compact
end
def changed_files
@@ -41,7 +52,7 @@ class EnvironmentStatus
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
- def build_change(file, sha)
+ def build_change(file)
public_path = project.public_path_for_source_path(file.new_path, sha)
return if public_path.nil?
@@ -53,4 +64,22 @@ class EnvironmentStatus
external_url: environment.external_url_for(file.new_path, sha)
}
end
+
+ def self.build_environments_status(mr, user, pipeline)
+ return [] unless pipeline.present?
+
+ find_environments(user, pipeline).map do |environment|
+ EnvironmentStatus.new(environment, mr, pipeline.sha)
+ end
+ 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/global_milestone.rb b/app/models/global_milestone.rb
index a6cebabe089..085ffd16c6a 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -34,50 +34,6 @@ class GlobalMilestone
new(title, child_milestones)
end
- def self.states_count(projects, group = nil)
- legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
- group_milestones_count = group_milestones_states_count(group)
-
- legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
- legacy_group_milestones_count + group_milestones_count
- end
- end
-
- def self.group_milestones_states_count(group)
- return STATE_COUNT_HASH unless group
-
- params = { group_ids: [group.id], state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- grouped_by_state = relation.reorder(nil).group(:state).count
-
- {
- opened: grouped_by_state['active'] || 0,
- closed: grouped_by_state['closed'] || 0,
- all: grouped_by_state.values.sum
- }
- end
-
- # Counts the legacy group milestones which must be grouped by title
- def self.legacy_group_milestone_states_count(projects)
- return STATE_COUNT_HASH unless projects
-
- params = { project_ids: projects.map(&:id), state: 'all' }
-
- relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
- project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
-
- opened = count_by_state(project_milestones_by_state_and_title, 'active')
- closed = count_by_state(project_milestones_by_state_and_title, 'closed')
- all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
-
- {
- opened: opened,
- closed: closed,
- all: all
- }
- end
-
def self.count_by_state(milestones_by_state_and_title, state)
milestones_by_state_and_title.count do |(milestone_state, _), _|
milestone_state == state
diff --git a/app/models/group.rb b/app/models/group.rb
index 612c546ca57..adb9169cfcd 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -41,6 +41,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
+ has_many :cluster_groups, class_name: 'Clusters::Group'
+ has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
+
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -366,7 +369,7 @@ class Group < Namespace
}
end
- def secret_variables_for(ref, project)
+ def ci_variables_for(ref, project)
list_of_ids = [self] + ancestors
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4ace5d3ab97..0de5e434b02 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -240,7 +240,8 @@ class Issue < ActiveRecord::Base
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)
+ 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
diff --git a/app/models/key.rb b/app/models/key.rb
index bdb83e12793..8f93418b88b 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -34,6 +34,10 @@ class Key < ActiveRecord::Base
after_destroy :post_destroy_hook
after_destroy :refresh_user_cache
+ def self.regular_keys
+ where(type: ['Key', nil])
+ end
+
def key=(value)
write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil)
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 97bf5d611c2..69c563545bb 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -7,7 +7,7 @@ class LfsObject < ActiveRecord::Base
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
- scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
+ scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) }
validates :oid, presence: true, uniqueness: true
@@ -26,7 +26,7 @@ class LfsObject < ActiveRecord::Base
end
def local_store?
- [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
+ file_store == LfsObjectUploader::Store::LOCAL
end
# rubocop: disable DestroyAll
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6559f94a696..735d9fba966 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
+ def merge_pipeline
+ return unless merged?
+
+ target_project.pipeline_for(target_branch, merge_commit_sha)
+ end
+
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
@@ -347,6 +353,15 @@ class MergeRequest < ActiveRecord::Base
end
end
+ # Returns true if there are commits that match at least one commit SHA.
+ def includes_any_commits?(shas)
+ if persisted?
+ merge_request_diff.commits_by_shas(shas).exists?
+ else
+ (commit_shas & shas).present?
+ end
+ end
+
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 02c6b650f33..bb6ff8921df 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -140,6 +140,12 @@ class MergeRequestDiff < ActiveRecord::Base
merge_request_diff_commits.map(&:sha)
end
+ def commits_by_shas(shas)
+ return [] unless shas.present?
+
+ merge_request_diff_commits.where(sha: shas)
+ end
+
def diff_refs=(new_diff_refs)
self.base_commit_sha = new_diff_refs&.base_sha
self.start_commit_sha = new_diff_refs&.start_sha
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 892a680f221..3cc8e2c44bb 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -145,7 +145,7 @@ class Milestone < ActiveRecord::Base
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
+ User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
end
def self.sort_by_attribute(method)
@@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base
sorted.with_order_id_desc
end
+ def self.states_count(projects, groups = nil)
+ return STATE_COUNT_HASH unless projects || groups
+
+ counts = Milestone
+ .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id))
+ .reorder(nil)
+ .group(:state)
+ .count
+
+ {
+ opened: counts['active'] || 0,
+ closed: counts['closed'] || 0,
+ all: counts.values.sum
+ }
+ end
+
##
# Returns the String necessary to reference this Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 599bedde27d..74d48d0a9af 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -83,7 +83,7 @@ class Namespace < ActiveRecord::Base
find_by('lower(path) = :value', value: path.downcase)
end
- # Case insensetive search for namespace by path or name
+ # Case insensitive search for namespace by path or name
def find_by_path_or_name(path)
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 989ebc4eca6..592efb714f3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -10,6 +10,7 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
+ include Redactable
include CacheMarkdownField
include AfterCommitQueue
include ResolvableNote
@@ -33,6 +34,8 @@ class Note < ActiveRecord::Base
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
+ redact_field :note
+
# Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102
alias_attribute :last_edited_at, :updated_at
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 207146479c0..73a58f2420e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,7 +3,7 @@
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
- add_authentication_token_field :token
+ add_authentication_token_field :token, digest: true, fallback: true
REDIS_EXPIRY_TIME = 3.minutes
@@ -33,16 +33,22 @@ class PersonalAccessToken < ActiveRecord::Base
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
- token = redis.get(redis_shared_state_key(user_id))
+ encrypted_token = redis.get(redis_shared_state_key(user_id))
redis.del(redis_shared_state_key(user_id))
- token
+ begin
+ Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ rescue => ex
+ logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
+ encrypted_token
+ end
end
end
def self.redis_store!(user_id, token)
+ encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token)
+
Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME)
- token
+ redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 382fb4f463a..fa995b5b061 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -181,7 +181,7 @@ class Project < ActiveRecord::Base
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it
- has_many :merge_requests, foreign_key: 'target_project_id'
+ has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
@@ -665,7 +665,7 @@ class Project < ActiveRecord::Base
remove_import_data
end
- # This method is overriden in EE::Project model
+ # This method is overridden in EE::Project model
def remove_import_data
import_data&.destroy
end
@@ -1811,7 +1811,7 @@ class Project < ActiveRecord::Base
.first
end
- def secret_variables_for(ref:, environment: nil)
+ def ci_variables_for(ref:, environment: nil)
# EE would use the environment
if protected_for?(ref)
variables
@@ -1829,7 +1829,7 @@ class Project < ActiveRecord::Base
end
def deployment_variables(environment: nil)
- deployment_platform(environment: environment)&.predefined_variables || []
+ deployment_platform(environment: environment)&.predefined_variables(project: self) || []
end
def auto_devops_variables
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index e1d342be188..7cff0e30e8d 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -9,7 +9,7 @@ class IssueTrackerService < Service
# Override this method on services that uses different patterns
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
- # overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
+ # overridden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
def self.reference_pattern(only_long: false)
if only_long
/(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 798944d0c06..3459ded7ccf 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -104,7 +104,12 @@ class KubernetesService < DeploymentService
{ success: false, result: err }
end
- def predefined_variables
+ # Project param was added on
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011,
+ # as a way to keep this service compatible with
+ # Clusters::Platforms::Kubernetes, it won't be used on this method
+ # as it's only needed for Clusters::Cluster.
+ def predefined_variables(project:)
config = YAML.dump(kubeconfig)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index e9533ee7c77..1c5846b4023 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -2,6 +2,7 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
+ include Redactable
include CacheMarkdownField
include Noteable
include Participable
@@ -18,6 +19,8 @@ class Snippet < ActiveRecord::Base
cache_markdown_field :description
cache_markdown_field :content
+ redact_field :description
+
# Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102
alias_attribute :last_edited_at, :updated_at
diff --git a/app/models/user.rb b/app/models/user.rb
index ca7fc3b058f..d3eb7162174 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,7 +28,7 @@ class User < ActiveRecord::Base
ignore_column :email_provider
ignore_column :authentication_token
- add_authentication_token_field :incoming_email_token
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
default_value_for :admin, false
@@ -88,7 +88,7 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
- has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys
@@ -463,7 +463,7 @@ class User < ActiveRecord::Base
def find_by_personal_access_token(token_string)
return unless token_string
- PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder
+ PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder
end
# Returns a user for the given SSH key.
@@ -941,12 +941,17 @@ class User < ActiveRecord::Base
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + 1.hour) < Time.now
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.now
else
false
end
end
+ def ldap_sync_time
+ # This number resides in this method so it can be redefined in EE.
+ 1.hour
+ end
+
def try_obtain_ldap_lease
# After obtaining this lease LDAP checks will be blocked for 600 seconds
# (10 minutes) for this user.
@@ -1138,7 +1143,7 @@ class User < ActiveRecord::Base
events = Event.select(:project_id)
.contributions.where(author_id: self)
.where("created_at > ?", Time.now - 1.year)
- .uniq
+ .distinct
.reorder(nil)
Project.where(id: events)
@@ -1464,15 +1469,6 @@ class User < ActiveRecord::Base
end
end
- def generate_token(token_field)
- if token_field == :incoming_email_token
- # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
- SecureRandom.hex.to_i(16).to_s(36)
- else
- super
- end
- end
-
def self.unique_internal(scope, username, email_pattern, &block)
scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
new file mode 100644
index 00000000000..6323c1b3389
--- /dev/null
+++ b/app/presenters/blob_presenter.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BlobPresenter < Gitlab::View::Presenter::Simple
+ presents :blob
+
+ def highlight(plain: nil)
+ blob.load_all_data! if blob.respond_to?(:load_all_data!)
+
+ Gitlab::Highlight.highlight(
+ blob.path,
+ blob.data,
+ language: blob.language_from_gitattributes,
+ plain: plain
+ )
+ end
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
new file mode 100644
index 00000000000..cff0e74d6ea
--- /dev/null
+++ b/app/presenters/clusterable_presenter.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class ClusterablePresenter < Gitlab::View::Presenter::Delegated
+ presents :clusterable
+
+ def self.fabricate(clusterable, **attributes)
+ presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
+ attributes_with_presenter_class = attributes.merge(presenter_class: presenter_class)
+
+ Gitlab::View::Presenter::Factory
+ .new(clusterable, attributes_with_presenter_class)
+ .fabricate!
+ end
+
+ def can_create_cluster?
+ can?(current_user, :create_cluster, clusterable)
+ end
+
+ def index_path
+ polymorphic_path([clusterable, :clusters])
+ end
+
+ def new_path
+ new_polymorphic_path([clusterable, :cluster])
+ end
+
+ def create_user_clusters_path
+ polymorphic_path([clusterable, :clusters], action: :create_user)
+ end
+
+ def create_gcp_clusters_path
+ polymorphic_path([clusterable, :clusters], action: :create_gcp)
+ end
+
+ def cluster_status_cluster_path(cluster, params = {})
+ raise NotImplementedError
+ end
+
+ def install_applications_cluster_path(cluster, application)
+ raise NotImplementedError
+ end
+
+ def cluster_path(cluster, params = {})
+ raise NotImplementedError
+ end
+end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index dfdd8e82f97..78d632eb77c 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -11,5 +11,13 @@ module Clusters
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
+
+ def show_path
+ if cluster.project_type?
+ project_cluster_path(project, cluster)
+ else
+ raise NotImplementedError
+ end
+ end
end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 29eaad759bb..a866e76df5a 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
runner_system_failure: 'There has been a runner system failure, please try again',
missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner',
- stale_schedule: 'Delayed job could not be executed by some reason, please try again'
+ stale_schedule: 'Delayed job could not be executed by some reason, please try again',
+ job_execution_timeout: 'The script exceeded the maximum execution time set for the job'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 3f565b826dd..1db6c9eff36 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -108,16 +108,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
namespace = source_project_namespace
branch = source_branch
- if source_branch_exists?
- namespace = link_to(namespace, project_path(source_project))
- branch = link_to(branch, project_tree_path(source_project, source_branch))
- end
+ namespace_link = source_branch_exists? ? link_to(namespace, project_path(source_project)) : ERB::Util.html_escape(namespace)
+ branch_link = source_branch_exists? ? link_to(branch, project_tree_path(source_project, source_branch)) : ERB::Util.html_escape(branch)
- if for_fork?
- namespace + ":" + branch
- else
- branch
- end
+ for_fork? ? "#{namespace_link}:#{branch_link}" : branch_link
end
def closing_issues_links
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
new file mode 100644
index 00000000000..12077b2e735
--- /dev/null
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ProjectClusterablePresenter < ClusterablePresenter
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_project_cluster_path(clusterable, cluster, params)
+ end
+
+ def install_applications_cluster_path(cluster, application)
+ install_applications_project_cluster_path(clusterable, cluster, application)
+ end
+
+ def cluster_path(cluster, params = {})
+ project_cluster_path(clusterable, cluster, params)
+ end
+end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 79cd3606aec..d61124fa787 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -176,7 +176,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(false,
_('New file'),
project_new_blob_path(project, default_branch || 'master'),
- 'new')
+ 'success')
end
end
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index 0db7875aa87..95833c3528f 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -12,7 +12,8 @@ class BuildActionEntity < Grape::Entity
end
expose :playable?, as: :playable
- expose :scheduled_at, if: -> (build) { build.scheduled? }
+ expose :scheduled?, as: :scheduled
+ expose :scheduled_at, if: -> (*) { scheduled? }
expose :unschedule_path, if: -> (build) { build.scheduled? } do |build|
unschedule_project_job_path(build.project, build)
@@ -25,4 +26,8 @@ class BuildActionEntity < Grape::Entity
def playable?
build.playable? && can?(request.current_user, :update_build, build)
end
+
+ def scheduled?
+ build.scheduled?
+ end
end
diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb
index 3dfa4f204c9..f87cc894d2f 100644
--- a/app/serializers/environment_status_entity.rb
+++ b/app/serializers/environment_status_entity.rb
@@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity
expose :id
expose :name
+ expose :status
expose :url do |es|
project_environment_path(es.project, es.environment)
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 0b19cb16955..aebbc18e32f 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -9,7 +9,7 @@ class JobEntity < Grape::Entity
expose :started?, as: :started
expose :build_path do |build|
- build.target_url || path_to(:namespace_project_job, build)
+ build_path(build)
end
expose :retry_path, if: -> (*) { retryable? } do |build|
@@ -17,7 +17,11 @@ class JobEntity < Grape::Entity
end
expose :cancel_path, if: -> (*) { cancelable? } do |build|
- path_to(:cancel_namespace_project_job, build)
+ path_to(
+ :cancel_namespace_project_job,
+ build,
+ { continue: { to: build_path(build) } }
+ )
end
expose :play_path, if: -> (*) { playable? } do |build|
@@ -29,6 +33,7 @@ class JobEntity < Grape::Entity
end
expose :playable?, as: :playable
+ expose :scheduled?, as: :scheduled
expose :scheduled_at, if: -> (*) { scheduled? }
expose :created_at
expose :updated_at
@@ -60,8 +65,12 @@ class JobEntity < Grape::Entity
build.detailed_status(request.current_user)
end
- def path_to(route, build)
- send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
+ def path_to(route, build, params = {})
+ send("#{route}_path", build.project.namespace, build.project, build, params) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def build_path(build)
+ build.target_url || path_to(:namespace_project_job, build)
end
def failed?
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 9ec24f799ef..f33a1654d5e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_commit_message
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 893b37b831a..f764536e762 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -99,7 +99,7 @@ module Auth
##
# Because we do not have two way communication with registry yet,
# we create a container repository image resource when push to the
- # registry is successfuly authorized.
+ # registry is successfully authorized.
#
def ensure_container_repository!(path, actions)
return if path.has_repository?
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index c6e955800af..270db4a52fd 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -8,10 +8,11 @@ module Clusters
@current_user, @params = user, params.dup
end
- def execute(project:, access_token: nil)
- raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project)
+ def execute(access_token: nil)
+ raise ArgumentError, 'Unknown clusterable provided' unless clusterable
+ raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?
- cluster_params = params.merge(user: current_user, projects: [project])
+ cluster_params = params.merge(user: current_user).merge(clusterable_params)
cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token
end
@@ -27,9 +28,20 @@ module Clusters
Clusters::Cluster.create(cluster_params)
end
+ def clusterable
+ @clusterable ||= params.delete(:clusterable)
+ end
+
+ def clusterable_params
+ case clusterable
+ when ::Project
+ { cluster_type: :project_type, projects: [clusterable] }
+ end
+ end
+
# EE would override this method
- def can_create_cluster?(project)
- project.clusters.empty?
+ def can_create_cluster?
+ clusterable.clusters.empty?
end
end
end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 6ee63db8eb9..3df43657fa0 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -11,8 +11,9 @@ module Clusters
configure_provider
create_gitlab_service_account!
configure_kubernetes
-
cluster.save!
+ configure_project_service_account
+
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue Kubeclient::HttpError => e
@@ -24,7 +25,10 @@ module Clusters
private
def create_gitlab_service_account!
- Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute
+ Clusters::Gcp::Kubernetes::CreateServiceAccountService.gitlab_creator(
+ kube_client,
+ rbac: create_rbac_cluster?
+ ).execute
end
def configure_provider
@@ -44,7 +48,20 @@ module Clusters
end
def request_kubernetes_token
- Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute
+ Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
+ kube_client,
+ Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
+ ).execute
+ end
+
+ def configure_project_service_account
+ kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
+
+ Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
end
def authorization_type
diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb
index d014d73b3e8..90ed529670c 100644
--- a/app/services/clusters/gcp/kubernetes.rb
+++ b/app/services/clusters/gcp/kubernetes.rb
@@ -3,11 +3,12 @@
module Clusters
module Gcp
module Kubernetes
- SERVICE_ACCOUNT_NAME = 'gitlab'
- SERVICE_ACCOUNT_NAMESPACE = 'default'
- SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token'
- CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
- CLUSTER_ROLE_NAME = 'cluster-admin'
+ GITLAB_SERVICE_ACCOUNT_NAME = 'gitlab'
+ GITLAB_SERVICE_ACCOUNT_NAMESPACE = 'default'
+ GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
+ GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
+ GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin'
+ PROJECT_CLUSTER_ROLE_NAME = 'edit'
end
end
end
diff --git a/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
new file mode 100644
index 00000000000..a888fab2789
--- /dev/null
+++ b/app/services/clusters/gcp/kubernetes/create_or_update_namespace_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Gcp
+ module Kubernetes
+ class CreateOrUpdateNamespaceService
+ def initialize(cluster:, kubernetes_namespace:)
+ @cluster = cluster
+ @kubernetes_namespace = kubernetes_namespace
+ @platform = cluster.platform
+ end
+
+ def execute
+ configure_kubernetes_namespace
+ create_project_service_account
+ configure_kubernetes_token
+
+ kubernetes_namespace.save!
+ rescue ::Kubeclient::HttpError => err
+ raise err unless err.error_code = 404
+ end
+
+ private
+
+ attr_reader :cluster, :kubernetes_namespace, :platform
+
+ def configure_kubernetes_namespace
+ kubernetes_namespace.configure_predefined_credentials
+ end
+
+ def create_project_service_account
+ Clusters::Gcp::Kubernetes::CreateServiceAccountService.namespace_creator(
+ platform.kubeclient,
+ service_account_name: kubernetes_namespace.service_account_name,
+ service_account_namespace: kubernetes_namespace.namespace,
+ rbac: platform.rbac?
+ ).execute
+ end
+
+ def configure_kubernetes_token
+ kubernetes_namespace.service_account_token = fetch_service_account_token
+ end
+
+ def fetch_service_account_token
+ Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(
+ platform.kubeclient,
+ kubernetes_namespace.token_name,
+ kubernetes_namespace.namespace
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
index d17744591e6..dfc4bf7a358 100644
--- a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
+++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb
@@ -4,46 +4,96 @@ module Clusters
module Gcp
module Kubernetes
class CreateServiceAccountService
- attr_reader :kubeclient, :rbac
-
- def initialize(kubeclient, rbac:)
+ def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient
+ @service_account_name = service_account_name
+ @service_account_namespace = service_account_namespace
+ @token_name = token_name
@rbac = rbac
+ @namespace_creator = namespace_creator
+ @role_binding_name = role_binding_name
+ end
+
+ def self.gitlab_creator(kubeclient, rbac:)
+ self.new(
+ kubeclient,
+ service_account_name: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAME,
+ service_account_namespace: Clusters::Gcp::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE,
+ token_name: Clusters::Gcp::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ rbac: rbac
+ )
+ end
+
+ def self.namespace_creator(kubeclient, service_account_name:, service_account_namespace:, rbac:)
+ self.new(
+ kubeclient,
+ service_account_name: service_account_name,
+ service_account_namespace: service_account_namespace,
+ token_name: "#{service_account_namespace}-token",
+ rbac: rbac,
+ namespace_creator: true,
+ role_binding_name: "gitlab-#{service_account_namespace}"
+ )
end
def execute
+ ensure_project_namespace_exists if namespace_creator
kubeclient.create_service_account(service_account_resource)
kubeclient.create_secret(service_account_token_resource)
- kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac
+ create_role_or_cluster_role_binding if rbac
end
private
+ attr_reader :kubeclient, :service_account_name, :service_account_namespace, :token_name, :rbac, :namespace_creator, :role_binding_name
+
+ def ensure_project_namespace_exists
+ Gitlab::Kubernetes::Namespace.new(
+ service_account_namespace,
+ kubeclient
+ ).ensure_exists!
+ end
+
+ def create_role_or_cluster_role_binding
+ if namespace_creator
+ kubeclient.create_role_binding(role_binding_resource)
+ else
+ kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
+ end
+ end
+
def service_account_resource
- Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate
+ Gitlab::Kubernetes::ServiceAccount.new(
+ service_account_name,
+ service_account_namespace
+ ).generate
end
def service_account_token_resource
Gitlab::Kubernetes::ServiceAccountToken.new(
- SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate
+ token_name,
+ service_account_name,
+ service_account_namespace
+ ).generate
end
def cluster_role_binding_resource
subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }]
Gitlab::Kubernetes::ClusterRoleBinding.new(
- CLUSTER_ROLE_BINDING_NAME,
- CLUSTER_ROLE_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_BINDING_NAME,
+ Clusters::Gcp::Kubernetes::GITLAB_CLUSTER_ROLE_NAME,
subjects
).generate
end
- def service_account_name
- SERVICE_ACCOUNT_NAME
- end
-
- def service_account_namespace
- SERVICE_ACCOUNT_NAMESPACE
+ def role_binding_resource
+ Gitlab::Kubernetes::RoleBinding.new(
+ name: role_binding_name,
+ role_name: Clusters::Gcp::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
+ namespace: service_account_namespace,
+ service_account_name: service_account_name
+ ).generate
end
end
end
diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
index 9e09345c8dc..277cc4b788d 100644
--- a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
+++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb
@@ -4,10 +4,12 @@ module Clusters
module Gcp
module Kubernetes
class FetchKubernetesTokenService
- attr_reader :kubeclient
+ attr_reader :kubeclient, :service_account_token_name, :namespace
- def initialize(kubeclient)
+ def initialize(kubeclient, service_account_token_name, namespace)
@kubeclient = kubeclient
+ @service_account_token_name = service_account_token_name
+ @namespace = namespace
end
def execute
@@ -18,7 +20,7 @@ module Clusters
private
def get_secret
- kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json
+ kubeclient.get_secret(service_account_token_name, namespace).as_json
rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index ced87a1c37a..80de897e94b 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -24,8 +24,8 @@ class DeleteMergedBranchesService < BaseService
# rubocop: disable CodeReuse/ActiveRecord
def merge_request_branch_names
# reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY
- source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch)
- target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch)
+ source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch)
+ target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch)
(source_names + target_names).uniq
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3e8b9f84042..c388913ae65 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -3,6 +3,14 @@
class IssuableBaseService < BaseService
private
+ attr_accessor :params, :skip_milestone_email
+
+ def initialize(project, user = nil, params = {})
+ super
+
+ @skip_milestone_email = @params.delete(:skip_milestone_email)
+ end
+
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index b54b0bf6ef6..fba252b0bae 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -48,6 +48,8 @@ module Issues
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
+ handle_milestone_change(issue)
+
added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present?
@@ -91,6 +93,18 @@ module Issues
private
+ def handle_milestone_change(issue)
+ return if skip_milestone_email
+
+ return unless issue.previous_changes.include?('milestone_id')
+
+ if issue.milestone.nil?
+ notification_service.async.removed_milestone_issue(issue, current_user)
+ else
+ notification_service.async.changed_milestone_issue(issue, issue.milestone, current_user)
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def get_issue_if_allowed(id, board_group_id = nil)
return unless id
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
index e2ae4047941..159455f80f3 100644
--- a/app/services/keys/destroy_service.rb
+++ b/app/services/keys/destroy_service.rb
@@ -6,7 +6,7 @@ module Keys
key.destroy if destroy_possible?(key)
end
- # overriden in EE::Keys::DestroyService
+ # overridden in EE::Keys::DestroyService
def destroy_possible?(key)
true
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 52360f775dc..9cbc9fef529 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -40,7 +40,7 @@ module Labels
group_labels_applied_to_merge_requests
])
.reorder(nil)
- .uniq
+ .distinct
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index 8248f1441d7..d734571f835 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -10,7 +10,7 @@ module Members
end
def after_execute(args)
- # overriden in EE::Members modules
+ # overridden in EE::Members modules
end
private
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 7c88c9abb41..35a22449e34 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)
- merge_request_params = { source_branch: branch_name }
- url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
+ url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name)
+
{ branch_name: branch_name, url: url, new_merge_request: true }
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index fb44f809c41..70a67baa01c 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -49,6 +49,11 @@ module MergeRequests
end
end
+ # Overridden in EE.
+ def hooks_validation_pass?(_merge_request)
+ true
+ end
+
private
def error_check!
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b03d14fa3cc..53768ff2cbe 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -85,13 +85,10 @@ module MergeRequests
.where.not(target_project: @project).to_a
filter_merge_requests(merge_requests).each do |merge_request|
- if merge_request.source_branch == @push.branch_name || @push.force_push?
+ if branch_and_project_match?(merge_request) || @push.force_push?
+ merge_request.reload_diff(current_user)
+ elsif merge_request.includes_any_commits?(push_commit_ids)
merge_request.reload_diff(current_user)
- else
- mr_commit_ids = merge_request.commit_shas
- push_commit_ids = @commits.map(&:id)
- matches = mr_commit_ids & push_commit_ids
- merge_request.reload_diff(current_user) if matches.any?
end
merge_request.mark_as_unchecked
@@ -104,6 +101,15 @@ module MergeRequests
end
# rubocop: enable CodeReuse/ActiveRecord
+ def push_commit_ids
+ @push_commit_ids ||= @commits.map(&:id)
+ end
+
+ def branch_and_project_match?(merge_request)
+ merge_request.source_project == @project &&
+ merge_request.source_branch == @push.branch_name
+ end
+
def reset_merge_when_pipeline_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
end
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
index b4d48fe92ad..b47d8f3f63a 100644
--- a/app/services/merge_requests/reload_diffs_service.rb
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -36,7 +36,10 @@ module MergeRequests
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
# reloading the diff.
- MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ MergeRequestDiff
+ .where(merge_request: merge_request)
+ .preload(merge_request: :target_project)
+ .find_each do |merge_request_diff|
next if merge_request_diff == new_diff
cacheable_collection(merge_request_diff).clear_cache
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index b112edbce7f..aacaf10d09c 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -58,6 +58,8 @@ module MergeRequests
merge_request.mark_as_unchecked
end
+ handle_milestone_change(merge_request)
+
added_labels = merge_request.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_merge_request(
@@ -105,6 +107,18 @@ module MergeRequests
private
+ def handle_milestone_change(merge_request)
+ return if skip_milestone_email
+
+ return unless merge_request.previous_changes.include?('milestone_id')
+
+ if merge_request.milestone.nil?
+ notification_service.async.removed_milestone_merge_request(merge_request, current_user)
+ else
+ notification_service.async.changed_milestone_merge_request(merge_request, merge_request.milestone, current_user)
+ end
+ end
+
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 7cda802c120..87c7a282081 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -4,7 +4,7 @@ module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
Milestone.transaction do
- update_params = { milestone: nil }
+ update_params = { milestone: nil, skip_milestone_email: true }
milestone.issues.each do |issue|
Issues::UpdateService.new(parent, current_user, update_params).execute(issue)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 50fa373025b..fb9c18ea75d 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -129,6 +129,14 @@ class NotificationService
relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
+ def removed_milestone_issue(issue, current_user)
+ removed_milestone_resource_email(issue, current_user, :removed_milestone_issue_email)
+ end
+
+ def changed_milestone_issue(issue, new_milestone, current_user)
+ changed_milestone_resource_email(issue, new_milestone, current_user, :changed_milestone_issue_email)
+ end
+
# When create a merge request we should send an email to:
#
# * mr author
@@ -138,7 +146,6 @@ class NotificationService
# * users with custom level checked with "new merge request"
#
# In EE, approvers of the merge request are also included
- #
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email)
end
@@ -208,6 +215,14 @@ class NotificationService
relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end
+ def removed_milestone_merge_request(merge_request, current_user)
+ removed_milestone_resource_email(merge_request, current_user, :removed_milestone_merge_request_email)
+ end
+
+ def changed_milestone_merge_request(merge_request, new_milestone, current_user)
+ changed_milestone_resource_email(merge_request, new_milestone, current_user, :changed_milestone_merge_request_email)
+ end
+
def close_mr(merge_request, current_user)
close_resource_email(merge_request, current_user, :closed_merge_request_email)
end
@@ -500,6 +515,30 @@ class NotificationService
end
end
+ def removed_milestone_resource_email(target, current_user, method)
+ recipients = NotificationRecipientService.build_recipients(
+ target,
+ current_user,
+ action: 'removed_milestone'
+ )
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.user.id, target.id, current_user.id).deliver_later
+ end
+ end
+
+ def changed_milestone_resource_email(target, milestone, current_user, method)
+ recipients = NotificationRecipientService.build_recipients(
+ target,
+ current_user,
+ action: 'changed_milestone'
+ )
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.user.id, target.id, milestone, current_user.id).deliver_later
+ end
+ end
+
def reopen_resource_email(target, current_user, method, status)
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb
index 2060a263751..2985ba89014 100644
--- a/app/services/projects/move_project_authorizations_service.rb
+++ b/app/services/projects/move_project_authorizations_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectAuthorizationsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
index fb395ecb9a1..36afcd0c503 100644
--- a/app/services/projects/move_project_group_links_service.rb
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectGroupLinksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
index f28f44adc03..faf389241d2 100644
--- a/app/services/projects/move_project_members_service.rb
+++ b/app/services/projects/move_project_members_service.rb
@@ -3,7 +3,7 @@
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
-# the authorizations if neccessary
+# the authorizations if necessary
module Projects
class MoveProjectMembersService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 751aae2696d..eb431c36807 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -433,14 +433,14 @@ module QuickActions
end
end
- desc 'Add or substract spent time'
+ desc 'Add or subtract spent time'
explanation do |time_spent, time_spent_date|
if time_spent
if time_spent > 0
verb = 'Adds'
value = time_spent
else
- verb = 'Substracts'
+ verb = 'Subtracts'
value = -time_spent
end
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 00372887985..34803d005e3 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -11,13 +11,11 @@ module Search
@group = group
end
- # rubocop: disable CodeReuse/ActiveRecord
def projects
return Project.none unless group
return @projects if defined? @projects
@projects = super.inside_path(group.full_path)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index e402801a776..f34305e94fa 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -9,8 +9,8 @@
= render 'ci/variables/variable_row', form_field: 'variables', variable: variable
= render 'ci/variables/variable_row', form_field: 'variables'
.prepend-top-20
- %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' }
- %span.hide.js-secret-variables-save-loading-icon
+ %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' }
+ %span.hide.js-ci-variables-save-loading-icon
= icon('spinner spin')
= _('Save variables')
%button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } }
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index 243e8cd9ba0..7037c80aa6b 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -12,4 +12,4 @@
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
+ = link_to(s_('ClusterIntegration|Remove integration'), clusterable.cluster_path(@cluster), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 73cfea0ef92..73cfea0ef92 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
index 2d7f7c6b1fb..facbcb7fc59 100644
--- a/app/views/projects/clusters/_cluster.html.haml
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -2,7 +2,7 @@
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
- = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
+ = 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
@@ -16,7 +16,7 @@
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: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ 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
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
index b8a3556a206..800e76d92ef 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -7,6 +7,6 @@
- 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}
- - if can?(current_user, :create_cluster, @project)
+ - if clusterable.can_create_cluster?
.text-center
- = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success'
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 73b11d509d3..73b11d509d3 100644
--- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml
index d0a553e3414..5e451f60c9d 100644
--- a/app/views/projects/clusters/_integration_form.html.haml
+++ b/app/views/clusters/clusters/_integration_form.html.haml
@@ -1,4 +1,4 @@
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
%h5= s_('ClusterIntegration|Integration status')
@@ -13,7 +13,7 @@
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
.form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
- - if has_multiple_clusters?(@project)
+ - if has_multiple_clusters?
.form-group
%h5= s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
@@ -23,7 +23,7 @@
.form-group
= field.submit _('Save changes'), class: 'btn btn-success'
- - unless has_multiple_clusters?(@project)
+ - unless has_multiple_clusters?
%h5= s_('ClusterIntegration|Environment scope')
%p
%code *
diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml
index 3d10348212f..3d10348212f 100644
--- a/app/views/projects/clusters/_sidebar.html.haml
+++ b/app/views/clusters/clusters/_sidebar.html.haml
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 171ceeceb68..ad842036a62 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -12,14 +12,14 @@
%p= link_to('Select a different Google account', @authorize_url)
-= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: create_gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= form_errors(@gcp_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
- = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/clusters/clusters/gcp/_header.html.haml
index a2ad3cd64df..a2ad3cd64df 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/clusters/clusters/gcp/_header.html.haml
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
index 779c9c245c1..6021b220285 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/clusters/clusters/gcp/_show.html.haml
@@ -6,7 +6,7 @@
%span.input-group-append
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index a55de84b5cd..a55de84b5cd 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index a38003f5750..eeeef6bd824 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -19,9 +19,9 @@
.tab-content.gitlab-tab-content
.tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
- = render 'projects/clusters/gcp/header'
+ = render 'clusters/clusters/gcp/header'
- if @valid_gcp_token
- = render 'projects/clusters/gcp/form'
+ = render 'clusters/clusters/gcp/form'
- elsif @authorize_url
.signin-with-google
= link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
@@ -32,5 +32,5 @@
= s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
.tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
- = render 'projects/clusters/user/header'
- = render 'projects/clusters/user/form'
+ = render 'clusters/clusters/user/header'
+ = render 'clusters/clusters/user/form'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index eddd3613c5f..1e1157c34bd 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -1,24 +1,25 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Kubernetes Clusters", project_clusters_path(@project)
+- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path
- breadcrumb_title @cluster.name
- page_title _("Kubernetes Cluster")
+- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test?
-- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
+- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
- install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
- install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
- install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
- install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
- install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter),
+ install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
+ install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
+ 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),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
- manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
+ manage_prometheus_path: manage_prometheus_path } }
.js-cluster-application-notice
.flash-container
@@ -38,9 +39,9 @@
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- if @cluster.managed?
- = render 'projects/clusters/gcp/show'
+ = render 'clusters/clusters/gcp/show'
- else
- = render 'projects/clusters/user/show'
+ = render 'clusters/clusters/user/show'
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 54a6e685bb0..4e6232b69de 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,9 +1,9 @@
-= form_for @user_cluster, url: create_user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field|
= form_errors(@user_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
- - if has_multiple_clusters?(@project)
+ - if has_multiple_clusters?
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 749177fa6c1..749177fa6c1 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
index 5b57f7ceb7d..a871fef0240 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/clusters/clusters/user/_show.html.haml
@@ -1,4 +1,4 @@
-= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
+= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 684b51b8552..0904e44a658 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -1,12 +1,12 @@
- @hide_breadcrumbs = true
- @hide_top_links = true
-- page_title 'New Group'
-- header_title "Groups", dashboard_groups_path
+- page_title _('New Group')
+- header_title _("Groups"), dashboard_groups_path
+.page-title-holder
+ %h1.page-title= _('New group')
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = _('New group')
%p
- group_docs_path = help_page_path('user/group/index')
- group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path }
@@ -15,24 +15,29 @@
- subgroup_docs_path = help_page_path('user/group/subgroups/index')
- subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path }
= s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe }
+ %p
+ = _('Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group.')
.col-lg-9
= form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f|
= form_errors(@group)
= render 'shared/group_form', f: f, autofocus: true
- .form-group.row.group-description-holder
- = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
- .col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
-
- = render 'shared/old_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
+ .row
+ .form-group.group-description-holder.col-sm-12
+ = f.label :avatar, _("Group avatar"), class: 'label-bold'
+ %div
+ = render 'shared/choose_group_avatar_button', f: f
- = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
+ .form-group.col-sm-12
+ %label.label-bold
+ = _('Visibility level')
+ %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
- .form-group.row
- .offset-sm-2.col-sm-10
- = render 'shared/group_tips'
+ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
.form-actions
= f.submit 'Create group', class: "btn btn-success"
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 647948c7dff..a5e6abdba52 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -3,7 +3,7 @@
- expanded = Rails.env.test?
-%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
+%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Variables')
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 596fc3985b3..b7d69539eb7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -5,7 +5,7 @@
- else
- search_path_url = search_path
-%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
+%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm.js-navbar
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
diff --git a/app/views/notify/changed_milestone_issue_email.html.haml b/app/views/notify/changed_milestone_issue_email.html.haml
new file mode 100644
index 00000000000..7d5425fc72d
--- /dev/null
+++ b/app/views/notify/changed_milestone_issue_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ Milestone changed to
+ %strong= link_to(@milestone.name, @milestone_url)
diff --git a/app/views/notify/changed_milestone_issue_email.text.erb b/app/views/notify/changed_milestone_issue_email.text.erb
new file mode 100644
index 00000000000..c5fc0b61518
--- /dev/null
+++ b/app/views/notify/changed_milestone_issue_email.text.erb
@@ -0,0 +1 @@
+Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/changed_milestone_merge_request_email.html.haml b/app/views/notify/changed_milestone_merge_request_email.html.haml
new file mode 100644
index 00000000000..7d5425fc72d
--- /dev/null
+++ b/app/views/notify/changed_milestone_merge_request_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ Milestone changed to
+ %strong= link_to(@milestone.name, @milestone_url)
diff --git a/app/views/notify/changed_milestone_merge_request_email.text.erb b/app/views/notify/changed_milestone_merge_request_email.text.erb
new file mode 100644
index 00000000000..c5fc0b61518
--- /dev/null
+++ b/app/views/notify/changed_milestone_merge_request_email.text.erb
@@ -0,0 +1 @@
+Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/removed_milestone_issue_email.html.haml b/app/views/notify/removed_milestone_issue_email.html.haml
new file mode 100644
index 00000000000..7e9205b6491
--- /dev/null
+++ b/app/views/notify/removed_milestone_issue_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Milestone removed
diff --git a/app/views/notify/removed_milestone_issue_email.text.erb b/app/views/notify/removed_milestone_issue_email.text.erb
new file mode 100644
index 00000000000..0b83ed7a4c5
--- /dev/null
+++ b/app/views/notify/removed_milestone_issue_email.text.erb
@@ -0,0 +1 @@
+Milestone removed
diff --git a/app/views/notify/removed_milestone_merge_request_email.html.haml b/app/views/notify/removed_milestone_merge_request_email.html.haml
new file mode 100644
index 00000000000..7e9205b6491
--- /dev/null
+++ b/app/views/notify/removed_milestone_merge_request_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Milestone removed
diff --git a/app/views/notify/removed_milestone_merge_request_email.text.erb b/app/views/notify/removed_milestone_merge_request_email.text.erb
new file mode 100644
index 00000000000..0b83ed7a4c5
--- /dev/null
+++ b/app/views/notify/removed_milestone_merge_request_email.text.erb
@@ -0,0 +1 @@
+Milestone removed
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 156c0d05b02..7c378633667 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -46,7 +46,7 @@
Layout width
= f.select :layout, layout_choices, {}, class: 'form-control'
.form-text.text-muted
- Choose between fixed (max. 1200px) and fluid (100%) application layout.
+ Choose between fixed (max. 1280px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-bold' do
Default dashboard
@@ -56,6 +56,6 @@
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.form-text.text-muted
- Choose what content you want to see on a project’s overview page
+ Choose what content you want to see on a project’s overview page.
.form-group
= f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml
index 9bd4ef6ad0b..6a631fef1a9 100644
--- a/app/views/projects/blob/viewers/_highlight_embed.html.haml
+++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml
@@ -4,4 +4,6 @@
- blob.data.each_line.each_with_index do |_, index|
%span.diff-line-num= index + 1
.blob-content{ data: { blob_id: blob.id } }
- = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?)
+ %pre.code.highlight
+ %code
+ = blob.present.highlight
diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml
index a91df321ca0..26ad23da436 100644
--- a/app/views/projects/blob/viewers/_text.html.haml
+++ b/app/views/projects/blob/viewers/_text.html.haml
@@ -1 +1 @@
-= render 'shared/file_highlight', blob: viewer.blob, repository: @repository
+= render 'shared/file_highlight', blob: viewer.blob
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index f5685d3b50d..0b10c66777a 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -105,10 +105,10 @@
= icon('remove', class: 'cred')
- elsif job.scheduled?
.btn-group
- .btn.btn-default.has-tooltip{ disabled: true,
- title: job.scheduled_at }
+ .btn.btn-default{ disabled: true }
= sprite_icon('planning')
- = duration_in_numbers(job.execute_in)
+ %time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
+ = duration_in_numbers(job.execute_in)
- confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name }
= link_to play_project_job_path(job.project, job, return_to: request.original_url),
method: :post,
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 07fc9e1c682..3aff5538813 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -148,7 +148,7 @@
= link_to 'Archive project', archive_project_path(@project),
data: { confirm: "Are you sure that you want to archive this project?" },
method: :post, class: "btn btn-warning"
- .sub-section.rename-respository
+ .sub-section.rename-repository
%h4.warning-title
Rename repository
= render 'projects/errors'
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 75f35360e5e..936900a0087 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -36,7 +36,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
+ .nav-links.scrolling-tabs.quick-links
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index efc2d88172e..5d1bbb077af 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -21,6 +21,7 @@
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
+ window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests', anchor: 'troubleshooting')}';
#js-vue-mr-widget.mr-widget
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index c63ff070f70..95bba47802c 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -19,30 +19,23 @@
#js-pipeline-graph-vue
#js-tab-builds.tab-pane
- - if pipeline.yaml_errors.present?
- .bs-callout.bs-callout-danger
- %h4 Found errors in your .gitlab-ci.yml:
- %ul
- - pipeline.yaml_errors.split(",").each do |error|
- %li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
+ - if pipeline.legacy_stages.present?
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Job ID
+ %th Name
+ %th
+ %th Coverage
+ %th
+ = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
- .table-holder.pipeline-holder
- %table.table.ci-table.pipeline
- %thead
- %tr
- %th Status
- %th Job ID
- %th Name
- %th
- %th Coverage
- %th
- = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
-
- if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane.build-page
%table.table.responsive-table.ci-table.responsive-table-sm-rounded
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index ff0ed3ed30d..193d437dad1 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -9,6 +9,14 @@
- if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit
- = render "projects/pipelines/with_tabs", pipeline: @pipeline
+ - if @pipeline.builds.empty? && @pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can test your .gitlab-ci.yml in #{link_to "CI Lint", project_ci_lint_path(@project)}.
+ - else
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index d14360913a4..82c8ec088e5 100644
--- a/app/views/projects/settings/ci_cd/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
@@ -15,14 +15,14 @@
.col-md-2.text-center
Markdown
.col-md-10.code.js-syntax-highlight
- = highlight('.md', badge.to_markdown)
+ = highlight('.md', badge.to_markdown, language: 'markdown')
.row
%hr
.row
.col-md-2.text-center
HTML
.col-md-10.code.js-syntax-highlight
- = highlight('.html', badge.to_html)
+ = highlight('.html', badge.to_html, language: 'html')
.row
%hr
.row
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 283031b06da..f29ce4f5c06 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -22,7 +22,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
+ .nav-links.scrolling-tabs.quick-links
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 8b95bdf9747..b4ecd7bbae9 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -39,7 +39,7 @@
.blob-content
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?)
+ = highlight(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 8d64cb5d698..5073e6ad48f 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,5 +1,3 @@
-- repository = nil unless local_assigns.key?(:repository)
-
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
@@ -13,4 +11,6 @@
= link_icon
= i
.blob-content{ data: { blob_id: blob.id } }
- = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?)
+ %pre.code.highlight
+ %code
+ = blob.present.highlight
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index dbed4b94d61..973c756f496 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -2,10 +2,19 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-.form-group.row
- = f.label :path, class: 'col-form-label col-sm-2' do
- Group path
- .col-sm-10
+.row
+ .form-group.group-name-holder.col-sm-12
+ = f.label :name, class: 'label-bold' do
+ = _("Group name")
+ = f.text_field :name, placeholder: 'My Awesome Group', class: 'form-control input-lg',
+ required: true,
+ title: _('Please fill in a descriptive name for your group.'),
+ autofocus: true
+
+.row
+ .form-group.col-xs-12.col-sm-8
+ = f.label :path, class: 'label-bold' do
+ = _("Group URL")
.input-group.gl-field-error-anchor
.group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
.input-group-text
@@ -13,10 +22,10 @@
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
- = f.text_field :path, placeholder: 'open-source', class: 'form-control',
+ = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- title: 'Please choose a group path with no special characters.',
+ title: _('Please choose a group URL with no special characters.'),
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- if @group.persisted?
@@ -25,23 +34,17 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
-.form-group.row.group-name-holder
- = f.label :name, class: 'col-form-label col-sm-2' do
- Group name
- .col-sm-10
- = f.text_field :name, class: 'form-control',
- required: true,
- title: 'You can choose a descriptive name different from the path.'
-
- if @group.persisted?
- .form-group.row.group-name-holder
- = f.label :id, class: 'col-form-label col-sm-2' do
- = _("Group ID")
- .col-sm-10
+ .row
+ .form-group.group-name-holder.col-sm-8
+ = f.label :id, class: 'label-bold' do
+ = _("Group ID")
= f.text_field :id, class: 'form-control', readonly: true
-.form-group.row.group-description-holder
- = f.label :description, class: 'col-form-label col-sm-2'
- .col-sm-10
+.row
+ .form-group.group-description-holder.col-sm-8
+ = f.label :description, class: 'label-bold' do
+ = _("Group description")
+ %span (optional)
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 6138914206b..19159684420 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -19,13 +19,13 @@
":value" => "label.id" }
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
- "v-bind:data-selected" => "selectedLabels",
+ ":data-selected" => "selectedLabels",
+ ":data-labels" => "issue.assignableLabelsEndpoint",
data: { toggle: "dropdown",
field_name: "issue[label_names][]",
show_no: "true",
show_any: "true",
project_id: @project&.try(:id),
- labels: labels_filter_path_with_defaults,
namespace_path: @namespace_path,
project_path: @project.try(:path) } }
%span.dropdown-toggle-text
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index cb45928d9a5..d27f79dc404 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -61,7 +61,10 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- = _('No Assignee')
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
@@ -81,7 +84,7 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link{ type: 'button' }
= _('Upcoming')
- %li.filter-dropdown-item{ 'data-value' => 'started' }
+ %li.filter-dropdown-item{ data: { value: 'started' } }
%button.btn.btn-link{ type: 'button' }
= _('Started')
%li.divider.droplab-item-ignore
@@ -102,6 +105,14 @@
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'none' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 10ffe8dd37f..5295e656ab0 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -24,7 +24,7 @@
.block.milestone
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true')
- %span.milestone-title
+ %span.milestone-title.collapse-truncated-title
- if issuable.milestone
= issuable.milestone.title
- else
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index b89194bcc67..3b5c13ed93a 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -21,3 +21,5 @@
- if params[:visibility_level].present?
= hidden_field_tag :visibility_level, params[:visibility_level]
+
+ = render_if_exists 'shared/projects/search_fields'
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index ddc089b0bd7..52c7bc47ca7 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -36,7 +36,7 @@
%li
.code.js-syntax-highlight.sherlock-code
:preserve
- #{highlight("#{@query.id}.sql", @query.formatted_query)}
+ #{highlight("#{@query.id}.sql", @query.formatted_query, language: 'sql')}
.card
.card-header
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index c1ec4b91bb6..5e224f3aa0e 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -17,7 +17,7 @@
= t('sherlock.milliseconds')
%td
.code.js-syntax-highlight.sherlock-code
- = highlight("#{query.id}.sql", query.formatted_query)
+ = highlight("#{query.id}.sql", query.formatted_query, language: 'sql')
%td
= link_to(t('sherlock.view'),
sherlock_transaction_query_path(@transaction, query),
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f21789de37d..a66a6f4c777 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -28,6 +28,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address
+- gcp_cluster:cluster_platform_configure
- github_import_advance_stage
- github_importer:github_import_import_diff_note
diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_platform_configure_worker.rb
new file mode 100644
index 00000000000..68e8335a09d
--- /dev/null
+++ b/app/workers/cluster_platform_configure_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class ClusterPlatformConfigureWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ def perform(cluster_id)
+ Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
+ next unless cluster.cluster_project
+
+ kubernetes_namespace = cluster.find_or_initialize_kubernetes_namespace(cluster.cluster_project)
+
+ Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
+ end
+
+ rescue ::Kubeclient::HttpError => err
+ Rails.logger.error "Failed to create/update Kubernetes Namespace. id: #{kubernetes_namespace.id} message: #{err.message}"
+ end
+end
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 59de7903c1c..3d5894b73ec 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -9,6 +9,8 @@ class ClusterProvisionWorker
cluster.provider.try do |provider|
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end
+
+ ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user?
end
end
end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index cd2ceb8dcdf..2b49860025a 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -14,7 +14,7 @@ module Gitlab
INTERVAL = 30.seconds.to_i
# The number of seconds to wait (while blocking the thread) before
- # continueing to the next waiter.
+ # continuing to the next waiter.
BLOCKING_WAIT_TIME = 5
# The known importer stages and their corresponding Sidekiq workers.
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 09a594cdb4e..72a1733a2a8 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -29,15 +29,14 @@ class PostReceive
def process_project_changes(post_received)
changes = []
refs = Set.new
+ @user = post_received.identify
- post_received.changes_refs do |oldrev, newrev, ref|
- @user ||= post_received.identify(newrev)
-
- unless @user
- log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
- return false # rubocop:disable Cop/AvoidReturnFromBlocks
- end
+ unless @user
+ log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
+ return false
+ end
+ post_received.changes_refs do |oldrev, newrev, ref|
if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
diff --git a/changelogs/unreleased/28249-add-pagination.yml b/changelogs/unreleased/28249-add-pagination.yml
new file mode 100644
index 00000000000..df15094405a
--- /dev/null
+++ b/changelogs/unreleased/28249-add-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Adds pagination to pipelines table in merge request page
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/34758-create-group-clusters.yml b/changelogs/unreleased/34758-create-group-clusters.yml
new file mode 100644
index 00000000000..50efde3cac3
--- /dev/null
+++ b/changelogs/unreleased/34758-create-group-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Adds model and migrations to enable group level clusters
+merge_request: 22307
+author:
+type: other
diff --git a/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml
new file mode 100644
index 00000000000..103419c1185
--- /dev/null
+++ b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml
@@ -0,0 +1,5 @@
+---
+title: Reword error message for internal CI unknown pipeline status
+merge_request: 22474
+author:
+type: changed
diff --git a/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
new file mode 100644
index 00000000000..d58d8da3a0e
--- /dev/null
+++ b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml
@@ -0,0 +1,5 @@
+---
+title: Adds trace of each access check when git push times out
+merge_request: 22265
+author:
+type: added
diff --git a/changelogs/unreleased/44012-filter-reactions-none-any.yml b/changelogs/unreleased/44012-filter-reactions-none-any.yml
new file mode 100644
index 00000000000..5d685010f8a
--- /dev/null
+++ b/changelogs/unreleased/44012-filter-reactions-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None / Any options to reactions filter
+merge_request: 22638
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml
new file mode 100644
index 00000000000..97052d01b24
--- /dev/null
+++ b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Hide all tables on Pipeline when no Jobs for the Pipeline
+merge_request: 18540
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml
new file mode 100644
index 00000000000..db374e10c36
--- /dev/null
+++ b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml
@@ -0,0 +1,5 @@
+---
+title: 'Create new group: Rename form fields and update UI'
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
new file mode 100644
index 00000000000..a15f1c033b3
--- /dev/null
+++ b/changelogs/unreleased/51259-ci-cd-gitlab-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Uses gitlab-ui components in jobs components
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
new file mode 100644
index 00000000000..a042debc28f
--- /dev/null
+++ b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml
@@ -0,0 +1,5 @@
+---
+title: If user was not found, service hooks won't run on post receive background job
+merge_request: 22519
+author:
+type: fixed
diff --git a/changelogs/unreleased/51527-xss-in-mr-source-branch.yml b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml
new file mode 100644
index 00000000000..dae277b6413
--- /dev/null
+++ b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fix XSS in merge request source branch name
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml
new file mode 100644
index 00000000000..9e99779d352
--- /dev/null
+++ b/changelogs/unreleased/51620-cannot-add-label-to-issue-from-board.yml
@@ -0,0 +1,4 @@
+title: Make Issue Board sidebar show project-specific labels based on selected Issue
+merge_request: 22475
+author:
+type: fixed
diff --git a/changelogs/unreleased/51716-create-kube-namespace.yml b/changelogs/unreleased/51716-create-kube-namespace.yml
new file mode 100644
index 00000000000..851e19c0a38
--- /dev/null
+++ b/changelogs/unreleased/51716-create-kube-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Extend RBAC by having a service account restricted to project's namespace
+merge_request: 22011
+author:
+type: other
diff --git a/changelogs/unreleased/52122-fix-broken-whitespace-button.yml b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml
new file mode 100644
index 00000000000..3f261eb2ac5
--- /dev/null
+++ b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken "Show whitespace changes" button on MRs.
+merge_request: 22539
+author:
+type: fixed
diff --git a/changelogs/unreleased/52382-filter-milestone-api-none-any.yml b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml
new file mode 100644
index 00000000000..a7559a25645
--- /dev/null
+++ b/changelogs/unreleased/52382-filter-milestone-api-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Standardize milestones filter in APIs to None / Any
+merge_request: 22637
+author: Heinrich Lee Yu
+type: changed
diff --git a/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml
new file mode 100644
index 00000000000..adf153f33ce
--- /dev/null
+++ b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Add None/Any option for assignee_id in search bar
+merge_request: 22599
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml
new file mode 100644
index 00000000000..052ef70c41a
--- /dev/null
+++ b/changelogs/unreleased/52548-links-in-tabs-of-the-labels-index-pages-ends-with-html.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug when links in tabs of the labels index pages ends with .html
+merge_request: 22716
+author:
+type: fixed
diff --git a/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml
new file mode 100644
index 00000000000..7586d7995b7
--- /dev/null
+++ b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Cache pipeline status per SHA.
+merge_request: 22589
+author:
+type: fixed
diff --git a/changelogs/unreleased/53052-mg-fix-broken-ie11.yml b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml
new file mode 100644
index 00000000000..c616efffa6b
--- /dev/null
+++ b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incompatibility with IE11 due to non-transpiled gitlab-ui components
+merge_request: 22695
+author:
+type: fixed
diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
deleted file mode 100644
index 605d3679159..00000000000
--- a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "fix link to enable usage ping from convdev index"
-merge_request: 22545
-author: Anand Capur
-type: fixed
diff --git a/changelogs/unreleased/53155-structured-logs-params-array.yml b/changelogs/unreleased/53155-structured-logs-params-array.yml
new file mode 100644
index 00000000000..4d4f68a5c84
--- /dev/null
+++ b/changelogs/unreleased/53155-structured-logs-params-array.yml
@@ -0,0 +1,5 @@
+---
+title: Use key-value pair arrays for API query parameter logging instead of hashes
+merge_request: 22623
+author:
+type: other
diff --git a/changelogs/unreleased/53227-empty-list.yml b/changelogs/unreleased/53227-empty-list.yml
new file mode 100644
index 00000000000..8b222145299
--- /dev/null
+++ b/changelogs/unreleased/53227-empty-list.yml
@@ -0,0 +1,6 @@
+---
+title: Only renders dropdown for review app changes when we have a list of files to
+ show. Otherwise will render the regular review app button
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/53270-remove-mousetrap-rails.yml b/changelogs/unreleased/53270-remove-mousetrap-rails.yml
new file mode 100644
index 00000000000..7214c81d73d
--- /dev/null
+++ b/changelogs/unreleased/53270-remove-mousetrap-rails.yml
@@ -0,0 +1,5 @@
+---
+title: Remove mousetrap-rails gem
+merge_request: 22647
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/53273-update-moment-to-2-22-2.yml b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml
new file mode 100644
index 00000000000..a6b40466927
--- /dev/null
+++ b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update moment to 2.22.2
+merge_request: 22648
+author: Takuya Noguchi
+type: security
diff --git a/changelogs/unreleased/ab-45608-stuck-mr-query.yml b/changelogs/unreleased/ab-45608-stuck-mr-query.yml
new file mode 100644
index 00000000000..3b64534e480
--- /dev/null
+++ b/changelogs/unreleased/ab-45608-stuck-mr-query.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to find stuck merge requests.
+merge_request: 22749
+author:
+type: performance
diff --git a/changelogs/unreleased/ac-post-merge-pipeline.yml b/changelogs/unreleased/ac-post-merge-pipeline.yml
new file mode 100644
index 00000000000..08322c9cb8a
--- /dev/null
+++ b/changelogs/unreleased/ac-post-merge-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Show post-merge pipeline in merge request page
+merge_request: 22292
+author:
+type: added
diff --git a/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
new file mode 100644
index 00000000000..c8488cbf200
--- /dev/null
+++ b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Add failure reason for execution timeout
+merge_request: 22224
+author:
+type: changed
diff --git a/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml
new file mode 100644
index 00000000000..a80b5a931b9
--- /dev/null
+++ b/changelogs/unreleased/add-scheduled-flag-to-job-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Add scheduled flag to job entity
+merge_request: 22710
+author:
+type: other
diff --git a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
new file mode 100644
index 00000000000..b89ba754952
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
@@ -0,0 +1,5 @@
+---
+title: Make new merge request URL more friendly when pushing code
+merge_request: 22526
+author: "@blackst0ne"
+type: changed
diff --git a/changelogs/unreleased/ccr-51520_change_milestone_email.yml b/changelogs/unreleased/ccr-51520_change_milestone_email.yml
new file mode 100644
index 00000000000..ce4beba2c5f
--- /dev/null
+++ b/changelogs/unreleased/ccr-51520_change_milestone_email.yml
@@ -0,0 +1,5 @@
+---
+title: Add email for milestone change
+merge_request: 22279
+author:
+type: added
diff --git a/changelogs/unreleased/drop-gcp-cluster-table.yml b/changelogs/unreleased/drop-gcp-cluster-table.yml
new file mode 100644
index 00000000000..15964ec2eaf
--- /dev/null
+++ b/changelogs/unreleased/drop-gcp-cluster-table.yml
@@ -0,0 +1,5 @@
+---
+title: Drop gcp_clusters table
+merge_request: 22713
+author:
+type: other
diff --git a/changelogs/unreleased/fix-53298.yml b/changelogs/unreleased/fix-53298.yml
new file mode 100644
index 00000000000..f0bf5470dc8
--- /dev/null
+++ b/changelogs/unreleased/fix-53298.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix #53298: JupyterHub restarts should work without errors'
+merge_request: 22671
+author: Amit Rathi
+type: fixed
diff --git a/changelogs/unreleased/fl-missing-i18n.yml b/changelogs/unreleased/fl-missing-i18n.yml
new file mode 100644
index 00000000000..d41a691e636
--- /dev/null
+++ b/changelogs/unreleased/fl-missing-i18n.yml
@@ -0,0 +1,5 @@
+---
+title: Adds missing i18n to pipelines table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
new file mode 100644
index 00000000000..ecbfc323080
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci-remain.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string for remaining lib/gitlab/ci/**/*.rb
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml
new file mode 100644
index 00000000000..a881c304f34
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen string for lib/gitlab/ci
+merge_request:
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/gl-ui-modal.yml b/changelogs/unreleased/gl-ui-modal.yml
new file mode 100644
index 00000000000..fbdb8260d24
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's modal from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-pagination.yml b/changelogs/unreleased/gl-ui-pagination.yml
new file mode 100644
index 00000000000..cf73d6a1f8f
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's pagination from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gl-ui-progress-bar.yml b/changelogs/unreleased/gl-ui-progress-bar.yml
new file mode 100644
index 00000000000..1e584dacd6f
--- /dev/null
+++ b/changelogs/unreleased/gl-ui-progress-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gitlab-ui's progress bar from global
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gt-fix-ide-typos-in-props.yml b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml
new file mode 100644
index 00000000000..a81b227c82f
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml
@@ -0,0 +1,5 @@
+---
+title: Fix IDE typos in props
+merge_request: 22685
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-fix-quick-links-button-styles.yml b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml
new file mode 100644
index 00000000000..4c1150631f8
--- /dev/null
+++ b/changelogs/unreleased/gt-fix-quick-links-button-styles.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quick links button styles
+merge_request: 22657
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml
new file mode 100644
index 00000000000..ca3b99e73ab
--- /dev/null
+++ b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate milestone title on collapsed sidebar
+merge_request: 22624
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/kinolaev-master-patch-91872.yml b/changelogs/unreleased/kinolaev-master-patch-91872.yml
new file mode 100644
index 00000000000..053e9101e39
--- /dev/null
+++ b/changelogs/unreleased/kinolaev-master-patch-91872.yml
@@ -0,0 +1,5 @@
+---
+title: Change HELM_HOST in Auto-DevOps template to work behind proxy
+merge_request: 22596
+author: Sergej Nikolaev <kinolaev@gmail.com>
+type: fixed
diff --git a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml b/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
deleted file mode 100644
index b61f47724fc..00000000000
--- a/changelogs/unreleased/mr-file-tree-inline-fluid-width-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed merge request fill tree toggling not respecting fluid width preference
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
new file mode 100644
index 00000000000..d0c8ed8001d
--- /dev/null
+++ b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2
+merge_request: 22600
+author:
+type: other
diff --git a/changelogs/unreleased/rails5-deprecated-uniq.yml b/changelogs/unreleased/rails5-deprecated-uniq.yml
new file mode 100644
index 00000000000..69a169100f0
--- /dev/null
+++ b/changelogs/unreleased/rails5-deprecated-uniq.yml
@@ -0,0 +1,5 @@
+---
+title: Replace deprecated uniq on a Relation with distinct
+merge_request: 22625
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml
new file mode 100644
index 00000000000..211d51a3d43
--- /dev/null
+++ b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml
@@ -0,0 +1,5 @@
+---
+title: "Secret Variables renamed to CI Variables in the codebase, to match UX"
+merge_request: 22414
+author: Marcel Amirault @ravlen
+type: changed \ No newline at end of file
diff --git a/changelogs/unreleased/redact-links-dev.yml b/changelogs/unreleased/redact-links-dev.yml
new file mode 100644
index 00000000000..338e7965465
--- /dev/null
+++ b/changelogs/unreleased/redact-links-dev.yml
@@ -0,0 +1,5 @@
+---
+title: Redact personal tokens in unsubscribe links.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/related_mrs.yml b/changelogs/unreleased/related_mrs.yml
new file mode 100644
index 00000000000..cc89e9d0cdb
--- /dev/null
+++ b/changelogs/unreleased/related_mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to list issue related merge requests
+merge_request: 21806
+author: Helmut Januschka
+type: added
diff --git a/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml
new file mode 100644
index 00000000000..ce52a487551
--- /dev/null
+++ b/changelogs/unreleased/remove-ci_enable_scheduled_build-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove `ci_enable_scheduled_build` feature flag
+merge_request: 22742
+author:
+type: other
diff --git a/changelogs/unreleased/replace-tooltip-in-markdown-component.yml b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml
new file mode 100644
index 00000000000..5047e75c06a
--- /dev/null
+++ b/changelogs/unreleased/replace-tooltip-in-markdown-component.yml
@@ -0,0 +1,5 @@
+---
+title: Replace tooltip in markdown component with gl-tooltip
+merge_request: 21989
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/rz_fix_milestone_count.yml b/changelogs/unreleased/rz_fix_milestone_count.yml
new file mode 100644
index 00000000000..1013b88e0bc
--- /dev/null
+++ b/changelogs/unreleased/rz_fix_milestone_count.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing count on Milestones
+merge_request: 21446
+author:
+type: fixed
diff --git a/changelogs/unreleased/security-2717-fix-issue-title-xss.yml b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml
new file mode 100644
index 00000000000..f2e638e5ab5
--- /dev/null
+++ b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml
@@ -0,0 +1,5 @@
+---
+title: Escape entity title while autocomplete template rendering to prevent XSS
+merge_request: 2556
+author:
+type: security
diff --git a/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
new file mode 100644
index 00000000000..4cebe814148
--- /dev/null
+++ b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Persist only SHA digest of PersonalAccessToken#token
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-kubeclient-ssrf.yml b/changelogs/unreleased/security-kubeclient-ssrf.yml
new file mode 100644
index 00000000000..45fc41029fc
--- /dev/null
+++ b/changelogs/unreleased/security-kubeclient-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Monkey kubeclient to not follow any redirects.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-hipchat-ssrf.yml b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml
new file mode 100644
index 00000000000..cdc95a34fcf
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent SSRF attacks in HipChat integration
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-issue-53153.yml b/changelogs/unreleased/sh-fix-issue-53153.yml
new file mode 100644
index 00000000000..ee51631f959
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-53153.yml
@@ -0,0 +1,5 @@
+---
+title: Fix extra merge request versions created from forked merge requests
+merge_request: 22611
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-search-relative-urls.yml b/changelogs/unreleased/sh-fix-search-relative-urls.yml
new file mode 100644
index 00000000000..2545e9ca553
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-search-relative-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search "all in GitLab" not working with relative URLs
+merge_request: 22644
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
new file mode 100644
index 00000000000..ac6ab7cc3f4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml
@@ -0,0 +1,5 @@
+---
+title: Validate Wiki attachments are valid temporary files
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml
new file mode 100644
index 00000000000..241b89c4633
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-merge-request-project-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce SQL queries needed to load open merge requests
+merge_request: 22709
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
new file mode 100644
index 00000000000..bea73f8d329
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-mr-commit-sha-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize merge request refresh by using the database to check commit SHAs
+merge_request: 22731
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-reload-diffs-service.yml b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml
new file mode 100644
index 00000000000..422102560ed
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-reload-diffs-service.yml
@@ -0,0 +1,5 @@
+---
+title: Significantly cut memory usage and SQL queries when reloading diffs
+merge_request: 22725
+author:
+type: performance
diff --git a/changelogs/unreleased/tc-index-lfs-objects-file-store.yml b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml
new file mode 100644
index 00000000000..90e80cb1ef1
--- /dev/null
+++ b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml
@@ -0,0 +1,5 @@
+---
+title: Enhance performance of counting local LFS objects
+merge_request: 22143
+author:
+type: performance
diff --git a/changelogs/unreleased/toggle-sidebar-alignment.yml b/changelogs/unreleased/toggle-sidebar-alignment.yml
new file mode 100644
index 00000000000..428fe61da9b
--- /dev/null
+++ b/changelogs/unreleased/toggle-sidebar-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Align toggle sidebar button across all browsers and OSs
+merge_request: 22771
+author:
+type: fixed
diff --git a/changelogs/unreleased/top_level_clusters_controller.yml b/changelogs/unreleased/top_level_clusters_controller.yml
new file mode 100644
index 00000000000..1fe1d048de4
--- /dev/null
+++ b/changelogs/unreleased/top_level_clusters_controller.yml
@@ -0,0 +1,6 @@
+---
+title: Change to top level controller for clusters so that we can use it for project
+ clusters (now) and group clusters (later)
+merge_request: 22438
+author:
+type: other
diff --git a/changelogs/unreleased/update_license_management_job.yml b/changelogs/unreleased/update_license_management_job.yml
new file mode 100644
index 00000000000..d6e56080e77
--- /dev/null
+++ b/changelogs/unreleased/update_license_management_job.yml
@@ -0,0 +1,5 @@
+---
+title: "Remove dind from license_management auto-devops job definition"
+merge_request: 22732
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-job-list-dynamic-timer.yml b/changelogs/unreleased/winh-job-list-dynamic-timer.yml
new file mode 100644
index 00000000000..333a974d6aa
--- /dev/null
+++ b/changelogs/unreleased/winh-job-list-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer for delayed jobs in job list
+merge_request: 22656
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml
new file mode 100644
index 00000000000..4ea1d3f8256
--- /dev/null
+++ b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml
@@ -0,0 +1,5 @@
+---
+title: Add dynamic timer for delayed jobs in pipelines list
+merge_request: 22621
+author:
+type: changed
diff --git a/config/application.rb b/config/application.rb
index 9074cf02c46..95b0f74a5a3 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -144,7 +144,7 @@ module Gitlab
config.assets.precompile << "errors.css"
# Import gitlab-svgs directly from vendored directory
- config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
+ config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist"
config.assets.precompile << "icons.svg"
config.assets.precompile << "icons.json"
config.assets.precompile << "illustrations/*.svg"
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 62760ffee3a..488728e26ab 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -461,7 +461,7 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :license
- - "@gitlab-org/gitlab-svgs"
+ - "@gitlab/svgs"
- MIT
- :who: Tim Zallmann
:why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a4db125f831..d37775e772d 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -587,7 +587,7 @@ production: &base
gitaly:
# Path to the directory containing Gitaly client executables.
client_path: /home/git/gitaly/bin
- # Default Gitaly authentication token. Can be overriden per storage. Can
+ # Default Gitaly authentication token. Can be overridden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
token:
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index c8d261d415e..468f80939d7 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -98,7 +98,11 @@ end
# check: https://github.com/rspec/rspec-mocks#settings-mocks-or-stubs-on-any-instance-of-a-class
#
# Related issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
-if Gitlab::Metrics.enabled? && !Rails.env.test?
+#
+# In development mode, we turn off eager loading when we're running
+# `rails generate migration` because eager loading short-circuits the
+# loading of our custom migration templates.
+if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && defined?(Rails::Generators))
require 'pathname'
require 'influxdb'
require 'connection_pool'
diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb
new file mode 100644
index 00000000000..aec265312bb
--- /dev/null
+++ b/config/initializers/hipchat_client_patch.rb
@@ -0,0 +1,14 @@
+# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
+module HipChat
+ class Client
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class Room
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+
+ class User
+ connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ end
+end
diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb
index 7f115268b37..2d9f439fdc0 100644
--- a/config/initializers/kubeclient.rb
+++ b/config/initializers/kubeclient.rb
@@ -13,4 +13,25 @@ class Kubeclient::Client
ns_prefix = build_namespace_prefix(namespace)
rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
end
+
+ # Monkey patch to set `max_redirects: 0`, so that kubeclient
+ # does not follow redirects and expose internal services.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53158
+ def create_rest_client(path = nil)
+ path ||= @api_endpoint.path
+ options = {
+ ssl_ca_file: @ssl_options[:ca_file],
+ ssl_cert_store: @ssl_options[:cert_store],
+ verify_ssl: @ssl_options[:verify_ssl],
+ ssl_client_cert: @ssl_options[:client_cert],
+ ssl_client_key: @ssl_options[:client_key],
+ proxy: @http_proxy_uri,
+ user: @auth_options[:username],
+ password: @auth_options[:password],
+ open_timeout: @timeouts[:open],
+ read_timeout: @timeouts[:read],
+ max_redirects: 0
+ }
+ RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
+ end
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 795e5d4e6bc..0a43a1d9a6b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -8,6 +8,8 @@ en:
issue_link:
source: Source issue
target: Target issue
+ group:
+ path: Group URL
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
diff --git a/config/routes.rb b/config/routes.rb
index 8723a928cc3..d2d91647d0b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -80,9 +80,27 @@ Rails.application.routes.draw do
get 'ide' => 'ide#index'
get 'ide/*vueroute' => 'ide#index', format: false
+ draw :operations
draw :instance_statistics
end
+ concern :clusterable do
+ resources :clusters, only: [:index, :new, :show, :update, :destroy] do
+ collection do
+ post :create_user
+ post :create_gcp
+ end
+
+ member do
+ scope :applications do
+ post '/:application', to: 'clusters/applications#create', as: :install_applications
+ end
+
+ get :cluster_status, format: :json
+ end
+ end
+ end
+
draw :api
draw :sidekiq
draw :help
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 85872a4122a..387d2363552 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -149,9 +149,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope path: 'merge_requests', controller: 'merge_requests/creations' do
post '', action: :create, as: nil
- scope path: 'new', as: :new_merge_request do
- get '', action: :new
-
+ scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do
scope constraints: { format: nil }, action: :new do
get :diffs, defaults: { tab: 'diffs' }
get :pipelines, defaults: { tab: 'pipelines' }
@@ -165,6 +163,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :diff_for_path
get :branch_from
get :branch_to
+ get '', action: :new
end
end
@@ -207,20 +206,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :clusters, except: [:edit, :create] do
- collection do
- post :create_gcp
- post :create_user
- end
-
- member do
- get :status, format: :json
-
- scope :applications do
- post '/:application', to: 'clusters/applications#create', as: :install_applications
- end
- end
- end
+ concerns :clusterable
resources :environments, except: [:destroy] do
member do
diff --git a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
index 8fc558be733..b7b346cb10e 100644
--- a/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
+++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
@@ -45,7 +45,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
connection.exec_query(query).present?
end
- insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
+ insert_query = "INSERT INTO namespaces(owner_id, path, name, created_at, updated_at) VALUES(#{user_id}, '#{path}', '#{path}', NOW(), NOW())"
namespace_id = connection.insert_sql(insert_query)
create_route(namespace_id)
@@ -57,7 +57,7 @@ class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
id, path = row.values_at('id', 'path')
- execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
+ execute("INSERT INTO routes(source_id, source_type, path, name, created_at, updated_at) VALUES(#{id}, 'Namespace', '#{path}', '#{path}', NOW(), NOW())")
end
def set_notification_email(user_id)
diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
new file mode 100644
index 00000000000..203fcfe8eae
--- /dev/null
+++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column :personal_access_tokens, :token, :string, null: true
+
+ add_column :personal_access_tokens, :token_digest, :string
+ end
+
+ def down
+ remove_column :personal_access_tokens, :token_digest
+
+ change_column :personal_access_tokens, :token, :string, null: false
+ end
+end
diff --git a/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
new file mode 100644
index 00000000000..4300cd13a45
--- /dev/null
+++ b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToTokenDigestOnPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :personal_access_tokens, :token_digest, unique: true
+ end
+
+ def down
+ remove_concurrent_index :personal_access_tokens, :token_digest if index_exists?(:personal_access_tokens, :token_digest)
+ end
+end
diff --git a/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb
new file mode 100644
index 00000000000..d09543aa4cc
--- /dev/null
+++ b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToLfsObjectsFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :lfs_objects, :file_store
+ end
+
+ def down
+ remove_concurrent_index :lfs_objects, :file_store
+ end
+end
diff --git a/db/migrate/20181014203236_create_cluster_groups.rb b/db/migrate/20181014203236_create_cluster_groups.rb
new file mode 100644
index 00000000000..69382d5c851
--- /dev/null
+++ b/db/migrate/20181014203236_create_cluster_groups.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateClusterGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :cluster_groups do |t|
+ t.references :cluster, null: false, foreign_key: { on_delete: :cascade }
+ t.references :group, null: false, index: true
+
+ t.index [:cluster_id, :group_id], unique: true
+ t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
new file mode 100644
index 00000000000..191e7eb4fb3
--- /dev/null
+++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddClusterTypeToClusters < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ PROJECT_CLUSTER_TYPE = 3
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE)
+ end
+
+ def down
+ remove_column(:clusters, :cluster_type)
+ end
+end
diff --git a/db/migrate/20181031190559_drop_gcp_clusters_table.rb b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
new file mode 100644
index 00000000000..808d474b4fc
--- /dev/null
+++ b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class DropGcpClustersTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ drop_table :gcp_clusters
+ end
+
+ def down
+ create_table :gcp_clusters do |t|
+ # Order columns by best align scheme
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.references :user, foreign_key: { on_delete: :nullify }
+ t.references :service, foreign_key: { on_delete: :nullify }
+ t.integer :status
+ t.integer :gcp_cluster_size, null: false
+
+ # Timestamps
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+
+ # Enable/disable
+ t.boolean :enabled, default: true
+
+ # General
+ t.text :status_reason
+
+ # k8s integration specific
+ t.string :project_namespace
+
+ # Cluster details
+ t.string :endpoint
+ t.text :ca_cert
+ t.text :encrypted_kubernetes_token
+ t.string :encrypted_kubernetes_token_iv
+ t.string :username
+ t.text :encrypted_password
+ t.string :encrypted_password_iv
+
+ # GKE
+ t.string :gcp_project_id, null: false
+ t.string :gcp_cluster_zone, null: false
+ t.string :gcp_cluster_name, null: false
+ t.string :gcp_machine_type
+ t.string :gcp_operation_id
+ t.text :encrypted_gcp_token
+ t.string :encrypted_gcp_token_iv
+ end
+ end
+end
diff --git a/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb
new file mode 100644
index 00000000000..5d3ace54e5c
--- /dev/null
+++ b/db/migrate/20181101144347_add_index_for_stuck_mr_query.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+class AddIndexForStuckMrQuery < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'"
+ end
+
+ def down
+ remove_concurrent_index :merge_requests, [:id, :merge_jid], where: "merge_jid IS NOT NULL and state = 'locked'"
+ end
+end
diff --git a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
index c48f1c938d0..3ae4406ff96 100644
--- a/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
+++ b/db/post_migrate/20170717111152_cleanup_move_system_upload_folder_symlink.rb
@@ -13,7 +13,7 @@ class CleanupMoveSystemUploadFolderSymlink < ActiveRecord::Migration
say "Removing #{old_directory} -> #{new_directory} symlink"
FileUtils.rm(old_directory)
else
- say "Symlink #{old_directory} non existant, nothing to do."
+ say "Symlink #{old_directory} non existent, nothing to do."
end
end
diff --git a/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
new file mode 100644
index 00000000000..36be819b245
--- /dev/null
+++ b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb
@@ -0,0 +1,28 @@
+class ScheduleDigestPersonalAccessTokens < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ BATCH_SIZE = 10_000
+ MIGRATION = 'DigestColumn'
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ disable_ddl_transaction!
+
+ class PersonalAccessToken < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def up
+ PersonalAccessToken.where('token is NOT NULL').each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, ['PersonalAccessToken', :token, :token_digest, *range])
+ end
+ end
+
+ def down
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/post_migrate/20181014121030_enqueue_redact_links.rb b/db/post_migrate/20181014121030_enqueue_redact_links.rb
new file mode 100644
index 00000000000..1ee4703c88a
--- /dev/null
+++ b/db/post_migrate/20181014121030_enqueue_redact_links.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+class EnqueueRedactLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 1000
+ DELAY_INTERVAL = 5.minutes.to_i
+ MIGRATION = 'RedactLinks'
+
+ disable_ddl_transaction!
+
+ class Note < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Snippet < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'snippets'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def up
+ disable_statement_timeout do
+ schedule_migration(Note, 'note')
+ schedule_migration(Issue, 'description')
+ schedule_migration(MergeRequest, 'description')
+ schedule_migration(Snippet, 'description')
+ end
+ end
+
+ def down
+ # nothing to do
+ end
+
+ private
+
+ def schedule_migration(model, field)
+ link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%"
+
+ model.where("#{field} like ?", link_pattern).each_batch(of: BATCH_SIZE) do |batch, index|
+ start_id, stop_id = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, [model.name.demodulize, field, start_id, stop_id])
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7a75aafd7b0..1a8b556228d 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: 20181016152238) do
+ActiveRecord::Schema.define(version: 20181101144347) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -599,6 +599,14 @@ ActiveRecord::Schema.define(version: 20181016152238) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
+ create_table "cluster_groups", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "group_id", null: false
+ end
+
+ add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree
+ add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree
+
create_table "cluster_platforms_kubernetes", force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -654,6 +662,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do
t.boolean "enabled", default: true
t.string "name", null: false
t.string "environment_scope", default: "*", null: false
+ t.integer "cluster_type", limit: 2, default: 3, null: false
end
add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
@@ -909,35 +918,6 @@ ActiveRecord::Schema.define(version: 20181016152238) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
- create_table "gcp_clusters", force: :cascade do |t|
- t.integer "project_id", null: false
- t.integer "user_id"
- t.integer "service_id"
- t.integer "status"
- t.integer "gcp_cluster_size", null: false
- t.datetime_with_timezone "created_at", null: false
- t.datetime_with_timezone "updated_at", null: false
- t.boolean "enabled", default: true
- t.text "status_reason"
- t.string "project_namespace"
- t.string "endpoint"
- t.text "ca_cert"
- t.text "encrypted_kubernetes_token"
- t.string "encrypted_kubernetes_token_iv"
- t.string "username"
- t.text "encrypted_password"
- t.string "encrypted_password_iv"
- t.string "gcp_project_id", null: false
- t.string "gcp_cluster_zone", null: false
- t.string "gcp_cluster_name", null: false
- t.string "gcp_machine_type"
- t.string "gcp_operation_id"
- t.text "encrypted_gcp_token"
- t.string "encrypted_gcp_token_iv"
- end
-
- add_index "gcp_clusters", ["project_id"], name: "index_gcp_clusters_on_project_id", unique: true, using: :btree
-
create_table "gpg_key_subkeys", force: :cascade do |t|
t.integer "gpg_key_id", null: false
t.binary "keyid"
@@ -1158,6 +1138,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do
t.integer "file_store"
end
+ add_index "lfs_objects", ["file_store"], name: "index_lfs_objects_on_file_store", using: :btree
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
create_table "lfs_objects_projects", force: :cascade do |t|
@@ -1313,6 +1294,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
+ add_index "merge_requests", ["id", "merge_jid"], name: "index_merge_requests_on_id_and_merge_jid", where: "((merge_jid IS NOT NULL) AND ((state)::text = 'locked'::text))", using: :btree
add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree
add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
@@ -1539,7 +1521,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
- t.string "token", null: false
+ t.string "token"
t.string "name", null: false
t.boolean "revoked", default: false
t.date "expires_at"
@@ -1547,9 +1529,11 @@ ActiveRecord::Schema.define(version: 20181016152238) do
t.datetime "updated_at", null: false
t.string "scopes", default: "--- []\n", null: false
t.boolean "impersonation", default: false, null: false
+ t.string "token_digest"
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "programming_languages", force: :cascade do |t|
@@ -2372,6 +2356,8 @@ ActiveRecord::Schema.define(version: 20181016152238) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
+ add_foreign_key "cluster_groups", "clusters", on_delete: :cascade
+ add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "projects", on_delete: :cascade
@@ -2398,9 +2384,6 @@ ActiveRecord::Schema.define(version: 20181016152238) do
add_foreign_key "fork_network_members", "projects", on_delete: :cascade
add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
- add_foreign_key "gcp_clusters", "projects", on_delete: :cascade
- add_foreign_key "gcp_clusters", "services", on_delete: :nullify
- add_foreign_key "gcp_clusters", "users", on_delete: :nullify
add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
diff --git a/doc/README.md b/doc/README.md
index 03371226041..20fcd2e1724 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -165,6 +165,7 @@ configuration. Then customize everything from buildpacks to CI/CD.
- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
- [Protected variables](ci/variables/README.md#protected-variables)
- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
+- [Executable Runbooks](user/project/clusters/runbooks/index.md)
### Monitor
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 96803746637..481eb692674 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -61,7 +61,7 @@ on an Linux NFS server, do the following:
2. Restart the NFS server process. For example, on CentOS run `service nfs restart`.
-## AWS Elastic File System
+## Avoid using AWS's Elastic File System (EFS)
GitLab strongly recommends against using AWS Elastic File System (EFS).
Our support team will not be able to assist on performance issues related to
@@ -78,6 +78,23 @@ stored on a local volume.
For more details on another person's experience with EFS, see
[Amazon's Elastic File System: Burst Credits](https://rawkode.com/2017/04/16/amazons-elastic-file-system-burst-credits/)
+## Avoid using PostgreSQL with NFS
+
+GitLab strongly recommends against running your PostgreSQL database
+across NFS. The GitLab support team will not be able to assist on performance issues related to
+this configuration.
+
+Additionally, this configuration is specifically warned against in the
+[Postgres Documentation](https://www.postgresql.org/docs/current/static/creating-cluster.html#CREATING-CLUSTER-NFS):
+
+>PostgreSQL does nothing special for NFS file systems, meaning it assumes NFS behaves exactly like
+>locally-connected drives. If the client or server NFS implementation does not provide standard file
+>system semantics, this can cause reliability problems. Specifically, delayed (asynchronous) writes
+>to the NFS server can cause data corruption problems.
+
+For supported database architecture, please see our documentation on
+[Configuring a Database for GitLab HA](https://docs.gitlab.com/ee/administration/high_availability/database.html).
+
## NFS Client mount options
Below is an example of an NFS mount point defined in `/etc/fstab` we use on
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index b5d1ff698c6..dcee57def74 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -15,7 +15,7 @@ Omnibus GitLab packages.
> **Notes:**
> - Redis requires authentication for High Availability. See
-> [Redis Security](http://redis.io/topics/security) documentation for more
+> [Redis Security](https://redis.io/topics/security) documentation for more
> information. We recommend using a combination of a Redis password and tight
> firewall rules to secure your Redis service.
> - You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
@@ -82,7 +82,7 @@ When a **Master** fails to respond, it's the application's responsibility
for a new **Master**).
To get a better understanding on how to correctly set up Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+the [Redis Sentinel documentation](https://redis.io/topics/sentinel) first, as
failing to configure it correctly can lead to data loss or can bring your
whole cluster down, invalidating the failover effort.
@@ -885,8 +885,8 @@ Read more on High Availability:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
-[redis]: http://redis.io/
-[sentinel]: http://redis.io/topics/sentinel
+[redis]: https://redis.io/
+[sentinel]: https://redis.io/topics/sentinel
[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
[source]: ../../install/installation.md
[ce]: https://about.gitlab.com/downloads
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
index 5823c575251..2101d36d2b6 100644
--- a/doc/administration/high_availability/redis_source.md
+++ b/doc/administration/high_availability/redis_source.md
@@ -107,7 +107,7 @@ starting with `sentinel` prefix.
Assuming that the Redis Sentinel is installed on the same instance as Redis
master with IP `10.0.0.1` (some settings might overlap with the master):
-1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. [Install Redis Sentinel](https://redis.io/topics/sentinel)
1. Edit `/etc/redis/sentinel.conf`:
```conf
@@ -363,7 +363,7 @@ production:
port: 26379 # point to sentinel, not to redis port
```
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+When in doubt, please read [Redis Sentinel documentation](https://redis.io/topics/sentinel).
[gh-531]: https://github.com/redis/redis-rb/issues/531
[downloads]: https://about.gitlab.com/downloads
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 0e41a38b7e2..038e043281c 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -84,7 +84,7 @@ Introduced in GitLab 10.0, this file lives in
It helps you see requests made directly to the API. For example:
```json
-{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"}
+{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30}
```
This entry above shows an access to an internal endpoint to check whether an
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 57e861bc62e..0dc9d706120 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -37,11 +37,11 @@ GET /issues?my_reaction_emoji=star
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
@@ -151,11 +151,11 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search group issues against their `title` and `description` |
@@ -265,11 +265,11 @@ GET /projects/:id/issues?my_reaction_emoji=star
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search project issues against their `title` and `description` |
@@ -1113,6 +1113,93 @@ Example response:
}
```
+## List merge requests related to issue
+
+Get all the merge requests that are related to the issue.
+
+```
+GET /projects/:id/issues/:issue_id/related_merge_requests
+```
+
+| 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 |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+
+```sh
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/11/related_merge_requests
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 29,
+ "iid": 11,
+ "project_id": 1,
+ "title": "Provident eius eos blanditiis consequatur neque odit.",
+ "description": "Ut consequatur ipsa aspernatur quisquam voluptatum fugit. Qui harum corporis quo fuga ut incidunt veritatis. Autem necessitatibus et harum occaecati nihil ea.\r\n\r\ntwitter/flight#8",
+ "state": "opened",
+ "created_at": "2018-09-18T14:36:15.510Z",
+ "updated_at": "2018-09-19T07:45:13.089Z",
+ "target_branch": "v2.x",
+ "source_branch": "so_long_jquery",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "id": 14,
+ "name": "Verna Hills",
+ "username": "lawanda_reinger",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/de68a91aeab1cff563795fb98a0c2cc0?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/lawanda_reinger"
+ },
+ "assignee": {
+ "id": 19,
+ "name": "Jody Baumbach",
+ "username": "felipa.kuvalis",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/6541fc75fc4e87e203529bd275fafd07?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/felipa.kuvalis"
+ },
+ "source_project_id": 1,
+ "target_project_id": 1,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 27,
+ "iid": 2,
+ "project_id": 1,
+ "title": "v1.0",
+ "description": "Et tenetur voluptatem minima doloribus vero dignissimos vitae.",
+ "state": "active",
+ "created_at": "2018-09-18T14:35:44.353Z",
+ "updated_at": "2018-09-18T14:35:44.353Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "https://gitlab.example.com/twitter/flight/milestones/2"
+ },
+ "merge_when_pipeline_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "sha": "3b7b528e9353295c1c125dad281ac5b5deae5f12",
+ "merge_commit_sha": null,
+ "user_notes_count": 9,
+ "discussion_locked": null,
+ "should_remove_source_branch": null,
+ "force_remove_source_branch": false,
+ "web_url": "https://gitlab.example.com/twitter/flight/merge_requests/4",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
+ "squash": false
+ }
+]
+```
+
## List merge requests that will close issue on merge
Get all the merge requests that will close issue when merged.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 0291b7e00c2..f3cfe0ad218 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -30,10 +30,10 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -43,11 +43,11 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
-| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
+| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
```json
[
@@ -154,10 +154,10 @@ Parameters:
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -167,9 +167,9 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `source_branch` | string | no | Return merge requests with the given source branch |
-| `target_branch` | string | no | Return merge requests with the given target branch |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
@@ -266,11 +266,11 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
-| `id` | integer | yes | The ID of a group |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
-| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
+| `id` | integer | yes | The ID of a group |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
+| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
@@ -280,9 +280,9 @@ Parameters:
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
-| `source_branch` | string | no | Return merge requests with the given source branch |
-| `target_branch` | string | no | Return merge requests with the given target branch |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
diff --git a/doc/api/tags.md b/doc/api/tags.md
index f2a3f9f28d2..826900ca518 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -134,7 +134,7 @@ Parameters:
"description": "Amazing release. Wow"
},
"name": "v1.0.0",
- "target: "2695effb5807a22ff3d138d593fd856244e155e7",
+ "target": "2695effb5807a22ff3d138d593fd856244e155e7",
"message": null
}
```
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
index 9657f52159e..6aa0edd87b4 100644
--- a/doc/ci/examples/artifactory_and_gitlab/index.md
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -16,8 +16,8 @@ to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory
You'll create two different projects:
-- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep)
-- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app)
+- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep )
+- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app )
We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/).
We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it.
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index 0f79f7d1b17..bc948dc6ea9 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -63,4 +63,10 @@ are still maintained, they have been deprecated with GitLab 11.0 and may be remo
in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
configuration to reflect that change.
+CAUTION: **Caution:**
+Starting with GitLab 11.5, Container Scanning feature is licensed under the name `container_scanning`.
+While the old name `sast_container` is still maintained, it has been deprecated with GitLab 11.5 and
+may be removed in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change if you are using the `$GITLAB_FEATURES` environment variable.
+
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index b2c73caae2e..c0346d78141 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -398,10 +398,10 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams
- [Using Docker images documentation][using-docker]
- [Example project: Hello GitLab CI/CD on GitLab][hello-gitlab]
-[phoenix-site]: http://phoenixframework.org/ "Phoenix Framework"
+[phoenix-site]: https://phoenixframework.org/ "Phoenix Framework"
[phoenix-learning-guide]: https://hexdocs.pm/phoenix/learning.html "Phoenix Learning Guide"
-[phoenix-install]: http://www.phoenixframework.org/docs/installation "Phoenix Installation"
-[phoenix-mysql]: http://www.phoenixframework.org/docs/using-mysql "Phoenix with MySQL"
+[phoenix-install]: https://hexdocs.pm/phoenix/installation.html "Phoenix Installation"
+[phoenix-mysql]: https://hexdocs.pm/phoenix/ecto.html#using-mysql "Phoenix with MySQL"
[elixir-site]: http://elixir-lang.org/ "Elixir"
[elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix"
[elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation"
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 338368dbbc9..b76f9618fc9 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -31,7 +31,7 @@ Database: el_duderino
```
If you are wondering why we used `mysql` for the `Host`, read more at
-[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
+[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-mysql].
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 6c6e198a7c3..95722c027ba 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -379,7 +379,7 @@ let(:mutation) do
)
end
-it 'returns a successfull response' do
+it 'returns a successful response' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 1764e2d8b21..5b32b5cd46f 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -171,6 +171,7 @@ the feature you contribute through all of these steps.
1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant
1. Community questions answered
1. Answers to questions radiated (in docs/wiki/support etc.)
+1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions
If you add a dependency in GitLab (such as an operating system package) please
consider updating the following and note the applicability of each in your
@@ -185,7 +186,7 @@ merge request:
1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[testing]: ../testing_guide/index.md
+[testing]: ../testing_guide/index.md
---
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index 3d8da6accc1..533e2001300 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -3,7 +3,7 @@
We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository.
This repository is published on [npm][npm] and managed as a dependency via yarn.
You can browse all available Icons and Illustrations [here][svg-preview].
-To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`.
+To upgrade to a new version run `yarn upgrade @gitlab/svgs`.
## Icons
@@ -111,6 +111,6 @@ export default {
</template>
```
-[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs
+[npm]: https://www.npmjs.com/package/@gitlab/svgs
[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md
index dada59ce242..b65fbc9d958 100644
--- a/doc/development/rolling_out_changes_using_feature_flags.md
+++ b/doc/development/rolling_out_changes_using_feature_flags.md
@@ -152,14 +152,31 @@ in RC1, followed by the feature flag being removed in RC2. This in turn means
the feature will be stable by the time we publish a stable package around the
22nd of the month.
-## Undefined feature flags default to "on"
+## Implicit feature flags
-By default, the [`Project#feature_available?`][project-fa],
+The [`Project#feature_available?`][project-fa],
[`Namespace#feature_available?`][namespace-fa] (EE), and
-[`License.feature_available?`][license-fa] (EE) methods will check if the
-specified feature is behind a feature flag. Unless the feature is explicitly
-disabled or limited to a percentage of users, the feature flag check will
-default to `true`.
+[`License.feature_available?`][license-fa] (EE) methods all implicitly check for
+a feature flag by the same name as the provided argument.
+
+For example if a feature is license-gated, there's no need to add an additional
+explicit feature flag check since the flag will be checked as part of the
+`License.feature_available?` call. Similarly, there's no need to "clean up" a
+feature flag once the feature has reached general availability.
+
+You'd still want to use an explicit `Feature.enabled?` check if your new feature
+isn't gated by a License or Plan.
+
+[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68
+[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
+[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
+
+### Undefined feature flags default to "on"
+
+An important side-effect of the [implicit feature
+flags][#implicit-feature-flags] mentioned above is that unless the feature is
+explicitly disabled or limited to a percentage of users, the feature flag check
+will default to `true`.
As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready
@@ -171,7 +188,3 @@ to be shipped. You can do this via ChatOps:
Note that you can do this at any time, even before the merge request using the
flag has been merged!
-
-[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68
-[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
-[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 4292a17bfa5..4f4ff85fe1d 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -37,9 +37,9 @@ Review Apps are automatically deployed by each pipeline, both in
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`
- manual job and then retry the `review` job to redeploy the review app.
+- 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`
+ 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.
diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md
index 8348de4f8a2..ceb9ed56ac4 100644
--- a/doc/development/ux_guide/tips.md
+++ b/doc/development/ux_guide/tips.md
@@ -1,20 +1,16 @@
# Tips
-## Contents
-* [SVGs](#svgs)
-
----
-
## SVGs
When exporting SVGs, be sure to follow the following guidelines:
1. Convert all strokes to outlines.
-2. Use pathfinder tools to combine overlapping paths and create compound paths.
-3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
-4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+1. Use pathfinder tools to combine overlapping paths and create compound paths.
+1. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+1. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open your SVG in a text editor to ensure that it is clean.
-You can open your svg in a text editor to ensure that it is clean.
Incorrect files will look like this:
```xml
@@ -35,10 +31,10 @@ Incorrect files will look like this:
</svg>
```
-Correct file will look like this:
+Correct files will look like this:
```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
```
-> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo)
+> TODO: Checkout <https://github.com/svg/svgo>.
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
index 676392eacf2..d67695d75b4 100644
--- a/doc/install/digitaloceandocker.md
+++ b/doc/install/digitaloceandocker.md
@@ -1,7 +1,8 @@
# Digital Ocean and Docker Machine test environment
-## Warning. This guide is for quickly testing different versions of GitLab and
-## not recommended for ease of future upgrades or keeping the data you create.
+CAUTION: **Caution:**
+This guide is for quickly testing different versions of GitLab and not recommended for ease of
+future upgrades or keeping the data you create.
## Initial setup
@@ -12,92 +13,88 @@ locally on either macOS or Linux.
#### Install Docker Toolbox
-1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox)
+- <https://www.docker.com/products/docker-toolbox>
### On Linux
#### Install Docker Engine
-1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/)
+- <https://docs.docker.com/engine/installation/linux/>
#### Install Docker Machine
-1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/)
+- <https://docs.docker.com/machine/install-machine/>
-_The rest of the steps are identical for macOS and Linux_
+NOTE: **Note:**
+The rest of the steps are identical for macOS and Linux.
### Create new docker host
-1. Login to Digital Ocean
-1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens
+1. Login to Digital Ocean.
+1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>.
+ This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
-This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
+ NOTE: **Note:**
+ 4GB is the minimum requirement for a Docker host that will run more than one GitLab instance.
-**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance**
+ - RAM: 4GB
+ - Name: `gitlab-test-env-do`
+ - Driver: `digitalocean`
-+ RAM: 4GB
-+ Name: `gitlab-test-env-do`
-+ Driver: `digitalocean`
+1. Set the DO token:
+ ```sh
+ export DOTOKEN=<your generated token>
+ ```
-**Set the DO token** - Replace the string below with your generated token
+1. Create the machine:
-```
-export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248
-```
-
-**Create the machine**
-
-```
-docker-machine create \
- --driver digitalocean \
- --digitalocean-access-token=$DOTOKEN \
- --digitalocean-size "4gb" \
- gitlab-test-env-do
-```
-
-+ Resource: https://docs.docker.com/machine/drivers/digital-ocean/
+ ```sh
+ docker-machine create \
+ --driver digitalocean \
+ --digitalocean-access-token=$DOTOKEN \
+ --digitalocean-size "4gb" \
+ gitlab-test-env-do
+ ```
+Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>.
### Creating GitLab test instance
-
#### Connect your shell to the new machine
-
In this example we'll create a GitLab EE 8.10.8 instance.
-
First connect the docker client to the docker host you created previously.
-```
+```sh
eval "$(docker-machine env gitlab-test-env-do)"
```
You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
-
#### Create new GitLab container
-+ HTTP port: `8888`
-+ SSH port: `2222`
- + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG `
-+ Hostname: IP of docker host
-+ Container name: `gitlab-test-8.10`
-+ GitLab version: **EE** `8.10.8-ee.0`
+- HTTP port: `8888`
+- SSH port: `2222`
+ - Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG`
+- Hostname: IP of docker host
+- Container name: `gitlab-test-8.10`
+- GitLab version: **EE** `8.10.8-ee.0`
-##### Set up container settings
+##### Set up container settings
-```
+```sh
export SSH_PORT=2222
export HTTP_PORT=8888
export VERSION=8.10.8-ee.0
export NAME=gitlab-test-8.10
```
-##### Create container
-```
+##### Create container
+
+```sh
docker run --detach \
--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \
--hostname $(docker-machine ip gitlab-test-env-do) \
@@ -110,23 +107,20 @@ gitlab/gitlab-ee:$VERSION
##### Retrieve the docker host IP
-```
+```sh
docker-machine ip gitlab-test-env-do
# example output: 192.168.151.134
```
-
-+ Browse to: http://192.168.151.134:8888/
-
+Browse to: <http://192.168.151.134:8888/>.
##### Execute interactive shell/edit configuration
-
-```
+```sh
docker exec -it $NAME /bin/bash
```
-```
+```sh
# example commands
root@192:/# vi /etc/gitlab/gitlab.rb
root@192:/# gitlab-ctl reconfigure
@@ -134,6 +128,6 @@ root@192:/# gitlab-ctl reconfigure
#### Resources
-+ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/)
-+ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/)
-+ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+
+- <https://docs.gitlab.com/omnibus/docker/>.
+- <https://docs.docker.com/machine/get-started/>.
+- <https://docs.docker.com/machine/reference/ip/>.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 2b9cce6539f..76f5495ff78 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -9,37 +9,39 @@ You can only restore a backup to **exactly the same version and type (CE/EE)**
of GitLab on which it was created. The best way to migrate your repositories
from one server to another is through backup restore.
-## Backup
+## Requirements
-GitLab provides a simple command line interface to backup your whole installation,
-and is flexible enough to fit your needs.
+In order to be able to backup and restore, you need two essential tools
+installed on your system.
-### Requirements
+### Rsync
-* rsync
+If you installed GitLab:
-If you're using GitLab with the Omnibus package, you're all set. If you
-installed GitLab from source, make sure you have rsync installed.
+- Using the Omnibus package, you're all set.
+- From source, make sure `rsync` is installed:
-If you're using Ubuntu, you could run:
+ ```sh
+ # Debian/Ubuntu
+ sudo apt-get install rsync
-```
-sudo apt-get install -y rsync
-```
+ # RHEL/CentOS
+ sudo yum install rsync
+ ```
-* tar
+### Tar
Backup and restore tasks use `tar` under the hood to create and extract
archives. Ensure you have version 1.30 or above of `tar` available in your
system. To check the version, run:
-```
+```sh
tar --version
```
-### Backup timestamp
+## Backup timestamp
->**Note:**
+NOTE: **Note:**
In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to
`EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25`
would become `1493107454_2018_04_25_10.6.4-ce`.
@@ -54,30 +56,46 @@ available.
For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`,
then the timestamp is `1493107454_2018_04_25_10.6.4-ce`.
-### Creating a backup of the GitLab system
+## Creating a backup of the GitLab system
+
+GitLab provides a simple command line interface to backup your whole instance.
+It backs up your:
+
+- Database
+- Attachments
+- Git repositories data
+- CI/CD job output logs
+- CI/CD job artifacts
+- LFS objects
+- Container Registry images
+- GitLab Pages content
+
+CAUTION: **Warning:**
+GitLab does not back up any configuration files, SSL certificates, or system files.
+You are highly advised to [read about storing configuration files](#storing-configuration-files).
Use this command if you've installed GitLab with the Omnibus package:
-```
+```sh
sudo gitlab-rake gitlab:backup:create
```
Use this if you've installed GitLab from source:
-```
+```sh
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
If you are running GitLab within a Docker container, you can run the backup from the host:
-```
+```sh
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
-run the backup task on the gitlab application pod using kubectl
+run the backup task on the gitlab application pod using kubectl:
-```
+```sh
kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
```
@@ -110,9 +128,50 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Storing configuration files
+
+A backup performed by the [raketask GitLab provides](#creating-a-backup-of-the-gitlab-system)
+does **not** store your configuration files. The primary reason for this is that your
+database contains encrypted information for two-factor authentication, the CI/CD
+'secure variables', etc. Storing encrypted information along with its key in the
+same place defeats the purpose of using encryption in the first place.
+
+CAUTION: **Warning:**
+The secrets file is essential to preserve your database encryption key.
+
+At the very **minimum**, you must backup:
+
+For Omnibus:
+
+- `/etc/gitlab/gitlab-secrets.json`
+- `/etc/gitlab/gitlab.rb`
+
+For installation from source:
+
+- `/home/git/gitlab/config/secrets.yml`
+- `/home/git/gitlab/config/gitlab.yml`
+
+For [Docker installations](https://docs.gitlab.com/omnibus/docker/), you must
+back up the volume where the configuration files are stored. If you have created
+the GitLab container according to the documentation, it should be under
+`/srv/gitlab/config`.
+
+You may also want to back up any TLS keys and certificates, and your
+[SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+
+If you use Omnibus GitLab, see some additional information
+[to backup your configuration](https://docs.gitlab.com/omnibus/settings/backups.html).
+
+In the unlikely event that the secrets file is lost, see the
+[troubleshooting section](#when-the-secrets-file-is-lost).
+
+## Backup options
+
+The command line tool GitLab provides to backup your instance can take more options.
+
### Backup strategy option
-> **Note:** Introduced as an option in GitLab 8.17.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8728) in GitLab 8.17.
The default backup strategy is to essentially stream data from the respective
data locations to the backup using the Linux command `tar` and `gzip`. This works
@@ -129,8 +188,11 @@ so the problem doesn't compound, but it could be a considerable change for large
installations. This is why the `copy` strategy is not the default in 8.17.
To use the `copy` strategy instead of the default streaming strategy, specify
-`STRATEGY=copy` in the Rake task command. For example,
-`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`.
+`STRATEGY=copy` in the Rake task command. For example:
+
+```sh
+sudo gitlab-rake gitlab:backup:create STRATEGY=copy
+```
### Excluding specific directories from the backup
@@ -151,11 +213,15 @@ Use a comma to specify several options at the same time:
All wikis will be backed up as part of the `repositories` group. Non-existent wikis
will be skipped during a backup.
-```
-# use this command if you've installed GitLab with the Omnibus package
+For Omnibus GitLab packages:
+
+```sh
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+```
+
+For installations from source:
-# if you've installed GitLab from source
+```sh
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
```
@@ -208,7 +274,7 @@ This example can be used for a bucket in Amsterdam (AMS3).
1. [Reconfigure GitLab] for the changes to take effect
-CAUTION: **Warning:**
+NOTE: **Note:**
If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the
usage of backup encryption. Remove or comment the line that
contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces
@@ -370,33 +436,43 @@ backups will be copied to, and will be created if it does not exist. If the
directory that you want to copy the tarballs to is the root of your mounted
directory, just use `.` instead.
-For omnibus packages:
-```ruby
-gitlab_rails['backup_upload_connection'] = {
- :provider => 'Local',
- :local_root => '/mnt/backups'
-}
+For Omnibus GitLab packages:
-# The directory inside the mounted folder to copy backups to
-# Use '.' to store them in the root directory
-gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
-```
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['backup_upload_connection'] = {
+ :provider => 'Local',
+ :local_root => '/mnt/backups'
+ }
+
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
For installations from source:
-```yaml
- backup:
- # snip
- upload:
- # Fog storage connection settings, see http://fog.io/storage/ .
- connection:
- provider: Local
- local_root: '/mnt/backups'
- # The directory inside the mounted folder to copy backups to
- # Use '.' to store them in the root directory
- remote_directory: 'gitlab_backups'
-```
+1. Edit `home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ backup:
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: Local
+ local_root: '/mnt/backups'
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ remote_directory: 'gitlab_backups'
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
### Backup archive permissions
@@ -405,45 +481,56 @@ will have owner/group git:git and 0600 permissions by default.
This is meant to avoid other system users reading GitLab's data.
If you need the backup archives to have different permissions you can use the 'archive_permissions' setting.
-```
-# In /etc/gitlab/gitlab.rb, for omnibus packages
-gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable
-```
+For Omnibus GitLab packages:
-```
-# In gitlab.yml, for installations from source:
- backup:
- archive_permissions: 0644 # Makes the backup archives world-readable
-```
+1. Edit `/etc/gitlab/gitlab.rb`:
-### Storing configuration files
+ ```ruby
+ gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
+
+For installations from source:
-Please be informed that a backup does not store your configuration
-files. One reason for this is that your database contains encrypted
-information for two-factor authentication. Storing encrypted
-information along with its key in the same place defeats the purpose
-of using encryption in the first place!
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
-If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
-If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+ ```yaml
+ backup:
+ archive_permissions: 0644 # Makes the backup archives world-readable
+ ```
-At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
-`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
-`/home/git/gitlab/config/secrets.yml` (source) to preserve your database
-encryption key.
+1. [Restart GitLab] for the changes to take effect.
### Configuring cron to make daily backups
->**Note:**
+NOTE: **Note:**
The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files)
or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
-**For Omnibus installations**
+For Omnibus GitLab packages:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ ## Limit backup lifetime to 7 days - 604800 seconds
+ gitlab_rails['backup_keep_time'] = 604800
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+Note that the `backup_keep_time` configuration option only manages local
+files. GitLab does not automatically prune old files stored in a third-party
+object storage (e.g., AWS S3) because the user may not have permission to list
+and delete files. We recommend that you configure the appropriate retention
+policy for your object storage. For example, you can configure [the S3 backup
+policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
-```
+```sh
sudo su -
crontab -e
```
@@ -455,26 +542,24 @@ There, add the following line to schedule the backup for everyday at 2 AM:
```
You may also want to set a limited lifetime for backups to prevent regular
-backups using all your disk space. To do this add the following lines to
-`/etc/gitlab/gitlab.rb` and reconfigure:
+backups using all your disk space.
-```
-# limit backup lifetime to 7 days - 604800 seconds
-gitlab_rails['backup_keep_time'] = 604800
-```
+---
-Note that the `backup_keep_time` configuration option only manages local
-files. GitLab does not automatically prune old files stored in a third-party
-object storage (e.g., AWS S3) because the user may not have permission to list
-and delete files. We recommend that you configure the appropriate retention
-policy for your object storage. For example, you can configure [the S3 backup
-policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
+For installations from source:
+
+1. Edit `home/git/gitlab/config/gitlab.yml`:
-**For installation from source**
+ ```yaml
+ backup:
+ ## Limit backup lifetime to 7 days - 604800 seconds
+ keep_time: 604800
+ ```
-```
-cd /home/git/gitlab
-sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
+1. [Restart GitLab] for the changes to take effect.
+
+
+```sh
sudo -u git crontab -e # Edit the crontab for the git user
```
@@ -711,5 +796,53 @@ Those objects have no influence on the database backup/restore but they give thi
For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql).
+### When the secrets file is lost
+
+If you have failed to [back up the secrets file](#storing-configuration-files),
+then users with 2FA enabled will not be able to log into GitLab. In that case,
+you need to [disable 2FA for everyone](../security/two_factor_authentication.md#disabling-2fa-for-everyone).
+
+In the case of CI/CD, if your project has secure variables set, you might experience
+some weird behavior, like stuck jobs or 500 errors. In that case, you can try
+deleting the `ci_variables` table from the database.
+
+CAUTION: **Warning:**
+Use the following commands at your own risk, and make sure you've taken a
+backup beforehand.
+
+1. Enter the Rails console:
+
+ For Omnibus GitLab packages:
+
+ ```sh
+ sudo gitlab-rails dbconsole
+ ```
+
+ For installations from source:
+
+ ```sh
+ sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production
+ ```
+
+1. Check the `ci_variables` table:
+
+ ```sql
+ SELECT * FROM public."ci_variables";
+ ```
+
+ Those are the variables that you need to delete.
+
+1. Drop the table:
+
+ ```sql
+ DELETE FROM ci_variables;
+ ```
+
+1. You may need to reconfigure or restart GitLab for the changes to take
+ effect.
+
+You should now be able to visit your project, and the jobs will start
+running again.
+
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 5db042326f3..c5b7813b285 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -8,163 +8,224 @@ you need a secure communication channel for sharing information.
The SSH protocol provides this security and allows you to authenticate to the
GitLab remote server without supplying your username or password each time.
-For a more detailed explanation of how the SSH protocol works, we advise you to
-read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
+For a more detailed explanation of how the SSH protocol works, read
+[this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
-## Locating an existing SSH key pair
+## Requirements
-Before generating a new SSH key pair check if your system already has one
-at the default location by opening a shell, or Command Prompt on Windows,
-and running the following command:
+The only requirement is to have the OpenSSH client installed on your system. This
+comes pre-installed on GNU/Linux and macOS, but not on Windows.
-**Windows Command Prompt:**
+Depending on your Windows version, there are different methods to work with
+SSH keys.
-```bash
-type %userprofile%\.ssh\id_rsa.pub
-```
+### Installing the SSH client for Windows 10
-**Git Bash on Windows / GNU/Linux / macOS / PowerShell:**
+Starting with Windows 10, you can
+[install the Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
+where you can run Linux distributions directly on Windows, without the overhead
+of a virtual machine. Once installed and set up, you'll have the Git and SSH
+clients at your disposal.
-```bash
-cat ~/.ssh/id_rsa.pub
-```
+### Installing the SSH client for Windows 8.1 and Windows 7
+
+The easiest way to install Git and the SSH client on Windows 8.1 and Windows 7
+is [Git for Windows](https://gitforwindows.com). It provides a BASH
+emulation (Git Bash) used for running Git from the command line and the
+`ssh-keygen` command that is useful to create SSH keys as you'll learn below.
+
+NOTE: **Alternative tools:**
+Although not explored in this page, you can use some alternative tools.
+[Cygwin](https://www.cygwin.com) is a large collection of GNU and open source
+tools which provide functionality similar to a Unix distribution.
+[PuttyGen](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html)
+provides a graphical user interface to [create SSH keys](https://tartarus.org/~simon/putty-snapshots/htmldoc/Chapter8.html#pubkey-puttygen).
+
+## Types of SSH keys and which to choose
+
+GitLab supports RSA, DSA, ECDSA, and ED25519 keys. Their difference lies on
+the signing algorithm, and some of them have advantages over the others. For
+more information, you can read this
+[nice article on ArchWiki](https://wiki.archlinux.org/index.php/SSH_keys#Choosing_the_authentication_key_type).
+We'll focus on ED25519 and RSA and here.
+
+NOTE: **Note:**
+As an admin, you can restrict
+[which keys should be permitted and their minimum length](../security/ssh_keys_restrictions.md).
+By default, all keys are permitted, which is also the case for
+[GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints).
-If you see a string starting with `ssh-rsa` you already have an SSH key pair
-and you can skip the generate portion of the next section and skip to the copy
-to clipboard step.
-If you don't see the string or would like to generate a SSH key pair with a
-custom name continue onto the next step.
+## ED25519 SSH keys
-Note that Public SSH key may also be named as follows:
+Following [best practices](https://linux-audit.com/using-ed25519-openssh-keys-instead-of-dsa-rsa-ecdsa/),
+you should always favor [ED25519](https://ed25519.cr.yp.to/) SSH keys, since they
+are more secure and have better performance over the other types.
-- `id_dsa.pub`
-- `id_ecdsa.pub`
-- `id_ed25519.pub`
+They were introduced in OpenSSH 6.5, so any modern OS should include the
+option to create them. If for any reason your OS or the GitLab instance you
+interact with doesn't support this, you can fallback to RSA.
+
+## RSA SSH keys
+
+RSA keys are the most common ones and therefore the most compatible with
+servers that may have an old OpenSSH version. Use them if the GitLab server
+doesn't work with ED25519 keys.
+
+The minimum key size is 1024 bits, defaulting to 2048. If you wish to generate a
+stronger RSA key pair, specify the `-b` flag with a higher bit value than the
+default.
+
+The old, default password encoding for SSH private keys keys is
+[insecure](https://latacora.singles/2018/08/03/the-default-openssh.html);
+it's only a single round of an MD5 hash. Since OpenSSH version 6.5, you should
+use the `-o` option to `ssh-keygen` to encode your private key in a new, more
+secure format.
+
+If you already have an RSA SSH key pair to use with GitLab, consider upgrading it
+to use the more secure password encryption format by using the following command
+on the private key:
+
+```bash
+ssh-keygen -o -f ~/.ssh/id_rsa
+```
## Generating a new SSH key pair
-1. To generate a new SSH key pair, use the following command:
+Before creating an SSH key pair, make sure to read about the
+[different types of keys](#types-of-ssh-keys-and-which-to-choose) to understand
+their differences.
+
+To create a new SSH key pair:
- **Git Bash on Windows / GNU/Linux / macOS:**
+1. Open a terminal on Linux or macOS, or Git Bash / WSL on Windows.
+1. Generate a new ED25519 SSH key pair:
```bash
- ssh-keygen -o -t rsa -C "your.email@example.com" -b 4096
+ ssh-keygen -t ed25519 -C "email@example.com"
```
- (Note: the `-o` option was introduced in 2014; if this command does not work for you, simply remove the `-o` option and try again)
+ Or, if you want to use RSA:
- **Windows:**
+ ```bash
+ ssh-keygen -o -t rsa -b 4096 -C "email@example.com"
+ ```
- Alternatively on Windows you can download
- [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
- and follow [this documentation article][winputty] to generate a SSH key pair.
+ The `-C` flag adds a comment in the key in case you have multiple of them
+ and want to tell which is which. It is optional.
1. Next, you will be prompted to input a file path to save your SSH key pair to.
+ If you don't already have an SSH key pair, use the suggested path by pressing
+ <kbd>Enter</kbd>. Using the suggested path will normally allow your SSH client
+ to automatically use the SSH key pair with no additional configuration.
- If you don't already have an SSH key pair use the suggested path by pressing
- enter. Using the suggested path will normally allow your SSH client
- to automatically use the SSH key pair with no additional configuration.
+ If you already have an SSH key pair with the suggested file path, you will need
+ to input a new file path and [declare what host](#working-with-non-default-ssh-key-pair-paths)
+ this SSH key pair will be used for in your `~/.ssh/config` file.
- If you already have a SSH key pair with the suggested file path, you will need
- to input a new file path and declare what host this SSH key pair will be used
- for in your `.ssh/config` file, see [**Working with non-default SSH key pair paths**](#working-with-non-default-ssh-key-pair-paths)
- for more information.
+1. Once the path is decided, you will be prompted to input a password to
+ secure your new SSH key pair. It's a best practice to use a password,
+ but it's not required and you can skip creating it by pressing
+ <kbd>Enter</kbd> twice.
-1. Once you have input a file path you will be prompted to input a password to
- secure your SSH key pair. It is a best practice to use a password for an SSH
- key pair, but it is not required and you can skip creating a password by
- pressing enter.
+ If, in any case, you want to add or change the password of your SSH key pair,
+ you can use the `-p`flag:
- NOTE: **Note:**
- If you want to change the password of your SSH key pair, you can use
- `ssh-keygen -p -o -f <keyname>`.
- The `-o` option was added in 2014, so if this command does not work for you,
- simply remove the `-o` option and try again.
+ ```
+ ssh-keygen -p -o -f <keyname>
+ ```
-## Adding a SSH key to your GitLab account
+Now, it's time to add the newly created public key to your GitLab account.
-1. The next step is to copy the public SSH key as we will need it afterwards.
+## Adding an SSH key to your GitLab account
- To copy your public SSH key to the clipboard, use the appropriate code below:
+1. Copy your **public** SSH key to the clipboard by using one of the commands below
+ depending on your Operating System:
**macOS:**
```bash
- pbcopy < ~/.ssh/id_rsa.pub
+ pbcopy < ~/.ssh/id_ed25519.pub
```
- **GNU/Linux (requires the xclip package):**
+ **WSL / GNU/Linux (requires the xclip package):**
```bash
- xclip -sel clip < ~/.ssh/id_rsa.pub
+ xclip -sel clip < ~/.ssh/id_ed25519.pub
```
- **Windows Command Line:**
+ **Git Bash on Windows:**
```bash
- type %userprofile%\.ssh\id_rsa.pub | clip
+ cat ~/.ssh/id_ed25519.pub | clip
```
- **Git Bash on Windows / Windows PowerShell:**
+ You can also open the key in a graphical editor and copy it from there,
+ but be careful not to accidentally change anything.
- ```bash
- cat ~/.ssh/id_rsa.pub | clip
- ```
-
-1. The final step is to add your public SSH key to GitLab.
+ NOTE: **Note:**
+ If you opted to create an RSA key, the name might differ.
- Navigate to the 'SSH Keys' tab in your 'Profile Settings'.
- Paste your key in the 'Key' section and give it a relevant 'Title'.
- Use an identifiable title like 'Work Laptop - Windows 7' or
- 'Home MacBook Pro 15'.
+1. Add your public SSH key to your GitLab account by clicking your avatar
+ in the upper right corner and selecting **Settings**. From there on,
+ navigate to **SSH Keys** and paste your public key in the "Key" section.
+ If you created the key with a comment, this will appear under "Title".
+ If not, give your key an identifiable title like _Work Laptop_ or
+ _Home Workstation_, and click **Add key**.
+ NOTE: **Note:**
If you manually copied your public SSH key make sure you copied the entire
- key starting with `ssh-rsa` and ending with your email.
+ key starting with `ssh-ed25519` (or `ssh-rsa`) and ending with your email.
+
+## Testing that everything is set up correctly
+
+To test whether your SSH key was added correctly, run the following command in
+your terminal (replacing `gitlab.com` with your GitLab's instance domain):
-1. Optionally you can test your setup by running `ssh -T git@example.com`
- (replacing `example.com` with your GitLab domain) and verifying that you
- receive a `Welcome to GitLab` message.
+```bash
+ssh -T git@gitlab.com
+```
+
+You should receive a _Welcome to GitLab, `@username`!_ message.
+
+If the welcome message doesn't appear, run SSH's verbose mode by replacing `-T`
+with `-vvvT` to understand where the error is.
## Working with non-default SSH key pair paths
If you used a non-default file path for your GitLab SSH key pair,
you must configure your SSH client to find your GitLab private SSH key
-for connections to your GitLab server (perhaps `gitlab.com`).
+for connections to GitLab.
-For your current terminal session you can do so using the following commands
+Open a terminal and use the following commands
(replacing `other_id_rsa` with your private SSH key):
-**Git Bash on Windows / GNU/Linux / macOS:**
-
```bash
eval $(ssh-agent -s)
ssh-add ~/.ssh/other_id_rsa
```
-To retain these settings you'll need to save them to a configuration file.
-For OpenSSH clients this is configured in the `~/.ssh/config` file for some
-operating systems.
+To retain these settings, you'll need to save them to a configuration file.
+For OpenSSH clients this is configured in the `~/.ssh/config` file. In this
+file you can set up configurations for multiple hosts, like GitLab.com, your
+own GitLab instance, GitHub, Bitbucket, etc.
+
Below are two example host configurations using their own SSH key:
-```
-# GitLab.com server
+```conf
+# GitLab.com
Host gitlab.com
-RSAAuthentication yes
-IdentityFile ~/.ssh/config/private-key-filename-01
+ Preferredauthentications publickey
+ IdentityFile ~/.ssh/gitlab_com_rsa
-# Private GitLab server
+# Private GitLab instance
Host gitlab.company.com
-RSAAuthentication yes
-IdentityFile ~/.ssh/config/private-key-filename
+ Preferredauthentications publickey
+ IdentityFile ~/.ssh/example_com_rsa
```
-Due to the wide variety of SSH clients and their very large number of
-configuration options, further explanation of these topics is beyond the scope
-of this document.
-
-Public SSH keys need to be unique, as they will bind to your account.
-Your SSH key is the only identifier you'll have when pushing code via SSH.
-That's why it needs to uniquely map to a single user.
+Public SSH keys need to be unique to GitLab, as they will bind to your account.
+Your SSH key is the only identifier you'll have when pushing code via SSH,
+that's why it needs to uniquely map to a single user.
## Deploy keys
@@ -240,8 +301,6 @@ not implicitly give any access just by setting them up.
How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
-[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
-
## SSH on the GitLab server
GitLab integrates with the system-installed SSH daemon, designating a user
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 9546f43eea8..73301394e9f 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
## Third-party resources
- [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth)
-- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin)
+- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin)
- [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4d4832184e2..96e788666a1 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -225,10 +225,11 @@ Auto DevOps at the project level.
### Enabling/disabling Auto DevOps at the project-level
-1. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+
1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Check the **Default to Auto DevOps pipeline** checkbox.
-1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
+1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable)
+1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain)
that will be used by Auto DevOps to [deploy your application](#auto-deploy)
and choose the [deployment strategy](#deployment-strategy).
1. Click **Save changes** for the changes to take effect.
@@ -246,12 +247,6 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
-### Disable Auto DevOps at the project level
-
-1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Uncheck the **Default to Auto DevOps pipeline** checkbox.
-1. Click **Save changes** for the changes to take effect.
-
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index b12e86cb0a9..6326aadcdf2 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -83,6 +83,9 @@ under which this application will be deployed.
![GitLab GKE cluster details](img/guide_gitlab_gke_details.png)
1. Once ready, click **Create Kubernetes cluster**.
+
+NOTE: **Note:**
+Do not select `f1-micro` from the **Machine type** dropdown. `f1-micro` machines cannot support a full GitLab installation.
After a couple of minutes, the cluster will be created. You can also see its
status on your [GCP dashboard](https://console.cloud.google.com/kubernetes).
diff --git a/doc/university/README.md b/doc/university/README.md
index 5edf92b3b09..f19b1ffd3d9 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -205,7 +205,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
### 4. External Articles
-1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 6ff27e495fb..6e0f71017c6 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index dccb6cbf071..f3a4d766522 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -6,91 +6,90 @@ comments: false
---
-# Agenda
+## Agenda
-1. Brief history of Git
-1. GitLab walkthrough
-1. Configure your environment
-1. Workshop
+1. Brief history of Git.
+1. GitLab walkthrough.
+1. Configure your environment.
+1. Workshop.
---
-# Git introduction
+## Git introduction
-https://git-scm.com/about
+<https://git-scm.com/about>
-- Distributed version control
- - Does not rely on connection to a central server
- - Many copies of the complete history
-- Powerful branching and merging
-- Adapts to nearly any workflow
-- Fast, reliable and stable file format
+- Distributed version control.
+ - Does not rely on connection to a central server.
+ - Many copies of the complete history.
+- Powerful branching and merging.
+- Adapts to nearly any workflow.
+- Fast, reliable and stable file format.
---
-# Help!
+## Help!
Use the tools at your disposal when you get stuck.
-- Use '`git help <command>`' command
-- Use Google
-- Read documentation at https://git-scm.com
+- Use '`git help <command>`' command.
+- Use Google.
+- Read documentation at <https://git-scm.com>.
---
-# GitLab Walkthrough
+## GitLab Walkthrough
![fit](logo.png)
---
-# Configure your environment
+## Configure your environment
- Windows: Install 'Git for Windows'
-> https://git-for-windows.github.io
+> <https://git-for-windows.github.io>
- Mac: Type '`git`' in the Terminal application.
> If it's not installed, it will prompt you to install it.
-- Debian: '`sudo apt-get install git-all`'
-or Red Hat '`sudo yum install git-all`'
+- Debian: '`sudo apt-get install git-all`' or Red Hat '`sudo yum install git-all`'
---
-# Git Workshop
+## Git Workshop
-## Overview
+### Overview
-1. Configure Git
-1. Configure SSH Key
-1. Create a project
-1. Committing
-1. Feature branching
-1. Merge requests
-1. Feedback and Collaboration
+1. Configure Git.
+1. Configure SSH Key.
+1. Create a project.
+1. Committing.
+1. Feature branching.
+1. Merge requests.
+1. Feedback and Collaboration.
---
-# Configure Git
+## Configure Git
-One-time configuration of the Git client
+One-time configuration of the Git client:
-```bash
+```sh
git config --global user.name "Your Name"
git config --global user.email you@example.com
```
---
-# Configure SSH Key
+## Configure SSH Key
-```bash
+```sh
ssh-keygen -t rsa -b 4096 -C "you@computer-name"
```
-```bash
+```sh
# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/you/.ssh/id_rsa):
@@ -102,31 +101,30 @@ The key fingerprint is:
39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
```
-Copy your public key and add it to your GitLab profile
+Copy your public key and add it to your GitLab profile:
-```bash
+```sh
cat ~/.ssh/id_rsa.pub
```
-```bash
+```sh
ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
```
---
-# Create a project
+## Create a project
-- Create a project in your user namespace
- - Choose to import from 'Any Repo by URL' and use
- https://gitlab.com/gitlab-org/training-examples.git
+- Create a project in your user namespace.
+ - Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git>.
- Create a '`development`' or '`workspace`' directory in your home directory.
-- Clone the '`training-examples`' project
+- Clone the '`training-examples`' project.
---
-# Commands
+## Commands (project)
-```
+```sh
mkdir ~/development
cd ~/development
@@ -141,37 +139,37 @@ cd training-examples
---
-# Git concepts
+## Git concepts
-**Untracked files**
+### Untracked files
New files that Git has not been told to track previously.
-**Working area**
+### Working area
Files that have been modified but are not committed.
-**Staging area**
+### Staging area
Modified files that have been marked to go in the next commit.
---
-# Committing
+## Committing
-1. Edit '`edit_this_file.rb`' in '`training-examples`'
-1. See it listed as a changed file (working area)
-1. View the differences
-1. Stage the file
-1. Commit
-1. Push the commit to the remote
-1. View the git log
+1. Edit '`edit_this_file.rb`' in '`training-examples`'.
+1. See it listed as a changed file (working area).
+1. View the differences.
+1. Stage the file.
+1. Commit.
+1. Push the commit to the remote.
+1. View the git log.
---
-# Commands
+## Commands (committing)
-```
+```sh
# Edit `edit_this_file.rb`
git status
git diff
@@ -183,29 +181,29 @@ git log
---
-# Feature branching
+## Feature branching
-- Efficient parallel workflow for teams
-- Develop each feature in a branch
-- Keeps changes isolated
-- Consider a 1-to-1 link to issues
-- Push branches to the server frequently
- - Hint: This is a cheap backup for your work-in-progress code
+- Efficient parallel workflow for teams.
+- Develop each feature in a branch.
+- Keeps changes isolated.
+- Consider a 1-to-1 link to issues.
+- Push branches to the server frequently.
+ - Hint: This is a cheap backup for your work-in-progress code.
---
-# Feature branching
+## Feature branching steps
-1. Create a new feature branch called 'squash_some_bugs'
+1. Create a new feature branch called 'squash_some_bugs'.
1. Edit '`bugs.rb`' and remove all the bugs.
-1. Commit
-1. Push
+1. Commit.
+1. Push.
---
-# Commands
+## Commands (feature branching)
-```
+```sh
git checkout -b squash_some_bugs
# Edit `bugs.rb`
git status
@@ -216,51 +214,50 @@ git push origin squash_some_bugs
---
-# Merge requests
+## Merge requests
-- When you want feedback create a merge request
-- Target is the ‘default’ branch (usually master)
-- Assign or mention the person you would like to review
-- Add 'WIP' to the title if it's a work in progress
-- When accepting, always delete the branch
-- Anyone can comment, not just the assignee
-- Push corrections to the same branch
+- When you want feedback create a merge request.
+- Target is the ‘default’ branch (usually master).
+- Assign or mention the person you would like to review.
+- Add 'WIP' to the title if it's a work in progress.
+- When accepting, always delete the branch.
+- Anyone can comment, not just the assignee.
+- Push corrections to the same branch.
---
-# Merge requests
+## Merge requests steps
-**Create your first merge request**
+Create your first merge request:
-1. Use the blue button in the activity feed
-1. View the diff (changes) and leave a comment
-1. Push a new commit to the same branch
-1. Review the changes again and notice the update
+1. Use the blue button in the activity feed.
+1. View the diff (changes) and leave a comment.
+1. Push a new commit to the same branch.
+1. Review the changes again and notice the update.
---
-# Feedback and Collaboration
+## Feedback and Collaboration
-- Merge requests are a time for feedback and collaboration
-- Giving feedback is hard
-- Be as kind as possible
-- Receiving feedback is hard
-- Be as receptive as possible
-- Feedback is about the best code, not the person. You are not your code
+- Merge requests are a time for feedback and collaboration.
+- Giving feedback is hard.
+- Be as kind as possible.
+- Receiving feedback is hard.
+- Be as receptive as possible.
+- Feedback is about the best code, not the person. You are not your code.
---
-# Feedback and Collaboration
+## Feedback and Collaboration resources
Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
-[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+<https://github.com/thoughtbot/guides/tree/master/code-review>.
-See GitLab merge requests for examples:
-[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+See GitLab merge requests for examples: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests>.
---
-# Explore GitLab projects
+## Explore GitLab projects
![fit](logo.png)
@@ -274,31 +271,29 @@ See GitLab merge requests for examples:
---
-# Tags
+## Tags
-- Useful for marking deployments and releases
-- Annotated tags are an unchangeable part of Git history
-- Soft/lightweight tags can be set and removed at will
-- Many projects combine an annotated release tag with a stable branch
-- Consider setting deployment/release tags automatically
+- Useful for marking deployments and releases.
+- Annotated tags are an unchangeable part of Git history.
+- Soft/lightweight tags can be set and removed at will.
+- Many projects combine an annotated release tag with a stable branch.
+- Consider setting deployment/release tags automatically.
---
-# Tags
-
-- Create a lightweight tag
-- Create an annotated tag
-- Push the tags to the remote repository
+## Tags steps
-**Additional resources**
+1. Create a lightweight tag.
+1. Create an annotated tag.
+1. Push the tags to the remote repository.
-[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+Additional resources: <http://git-scm.com/book/en/Git-Basics-Tagging>.
---
-# Commands
+## Commands (tags)
-```
+```sh
git checkout master
# Lightweight tag
@@ -313,31 +308,31 @@ git push origin --tags
---
-# Merge conflicts
+## Merge conflicts
-- Happen often
-- Learning to fix conflicts is hard
-- Practice makes perfect
+- Happen often.
+- Learning to fix conflicts is hard.
+- Practice makes perfect.
- Force push after fixing conflicts. Be careful!
---
-# Merge conflicts
+## Merge conflicts steps
1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
-1. Commit and push
+1. Commit and push.
1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
-1. Commit and push to master
-1. Create a merge request
+1. Commit and push to master.
+1. Create a merge request.
---
-# Merge conflicts
+## Merge conflicts commands
After creating a merge request you should notice that conflicts exist. Resolve
the conflicts locally by rebasing.
-```
+```sh
git rebase master
# Fix conflicts by editing the files.
@@ -350,7 +345,7 @@ git push origin <branch> -f
---
-# Rebase with squash
+## Rebase with squash
You may end up with a commit log that looks like this:
@@ -368,11 +363,11 @@ Squash these in to meaningful commits using an interactive rebase.
---
-# Rebase with squash
+## Rebase with squash commands
Squash the commits on the same branch we used for the merge conflicts step.
-```
+```sh
git rebase -i master
```
@@ -380,17 +375,17 @@ In the editor, leave the first commit as 'pick' and set others to 'fixup'.
---
-# Questions?
+## Questions?
![fit](logo.png)
Thank you for your hard work!
-**Additional Resources**
+## Additional Resources
-GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
-GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
-Pro git book [http://git-scm.com/book](http://git-scm.com/book)
-Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
-Code School tutorial [http://try.github.io/](http://try.github.io/)
-Contact Us at `subscribers@gitlab.com`
+- GitLab Documentation: <http://docs.gitlab.com/>.
+- GUI Clients: <http://git-scm.com/downloads/guis>.
+- 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/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 35a9d7adb28..bd0155dc712 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -42,7 +42,11 @@ not send any project names, usernames, or any other specific data. The
information from the usage ping is not anonymous, it is linked to the hostname
of the instance.
-You can view the exact JSON payload in the administration panel.
+You can view the exact JSON payload in the administration panel. To view the payload:
+
+1. Go to the **Admin area** (spanner symbol on the top bar).
+1. Expand **Settings** in the left sidebar and click on **Metrics and profiling**.
+1. Expand **Usage statistics** and click on the **Preview payload** button.
### Deactivate the usage ping
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index f9bdaea185b..96a509c4b21 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1,17 +1,8 @@
# Markdown
-## GitLab Flavored Markdown (GFM)
-
-> **Note:**
-> Not all of the GitLab-specific extensions to Markdown that are described in
-> this document currently work on our documentation website.
->
-> For the best result, we encourage you to check this document out as rendered
-> by GitLab: [markdown.md]
-
-_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the repositories are also processed with CommonMark. Older content in issues/comments are still processed using the [Redcarpet Ruby library][redcarpet]._
+This markdown guide is valid for GitLab's system markdown entries and files.
-_Where there are significant differences, we will try to call them out in this document._
+## GitLab Flavored Markdown (GFM)
GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification][commonmark-spec] (which is based on standard Markdown) in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
@@ -26,11 +17,28 @@ You can use GFM in the following areas:
- markdown documents inside the repository
You can also use other rich text files in GitLab. You might have to install a
-dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
+dependency to do so. Please see the [`github-markup` gem readme](https://github.com/gitlabhq/markup#markups) for more information.
+
+> **Notes:**
+>
+> For the best result, we encourage you to check this document out as rendered
+> by GitLab itself: [markdown.md]
+>
+> As of 11.1, GitLab uses the [CommonMark Ruby Library][commonmarker] for Markdown
+processing of all new issues, merge requests, comments, and other Markdown content
+in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the
+repositories are also processed with CommonMark. Older content in issues/comments
+are still processed using the [Redcarpet Ruby library][redcarpet].
+>
+> _Where there are significant differences, we will try to call them out in this document._
### Transitioning to CommonMark
-You may have Markdown documents in your repository that were written using some of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a slightly stricter syntax, these documents may now display a little strangely since we've transitioned to CommonMark. Numbered lists with nested lists in particular can be displayed incorrectly.
+You may have Markdown documents in your repository that were written using some
+of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a
+slightly stricter syntax, these documents may now display a little strangely
+since we've transitioned to CommonMark. Numbered lists with nested lists in
+particular can be displayed incorrectly.
It is usually quite easy to fix. In the case of a nested list such as this:
@@ -50,11 +58,18 @@ simply add a space to each nested item:
In the documentation below, we try to highlight some of the differences.
-If you have a need to view a document using RedCarpet, you can add the token `legacy_render=1` to the end of the url, like this:
+If you have a need to view a document using RedCarpet, you can add the token
+`legacy_render=1` to the end of the url, like this:
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md?legacy_render=1
-If you have a large volume of Markdown files, it can be tedious to determine if they will be displayed correctly or not. You can use the [diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and differences between how RedCarpet and CommonMark render the files. It can give you a great idea if anything needs to be changed - many times nothing will need to changed.
+If you have a large volume of Markdown files, it can be tedious to determine
+if they will be displayed correctly or not. You can use the
+[diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark)
+tool (not an officially supported product) to generate a list of files and
+differences between how RedCarpet and CommonMark render the files. It can give
+you a great idea if anything needs to be changed - many times nothing will need
+to changed.
### Newlines
@@ -63,7 +78,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline
GFM honors the markdown specification in how [paragraphs and line breaks are handled][commonmark-spec].
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+A paragraph is simply one or more consecutive lines of text, separated by one or
+more blank lines.
Line-breaks, or soft returns, are rendered if you end a line with two or more spaces:
<!-- (Do *NOT* remove the two ending whitespaces in the following line.) -->
@@ -85,7 +101,9 @@ Sugar is sweet
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
-It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
+It is not reasonable to italicize just _part_ of a word, especially when you're
+dealing with code and names that often appear with multiple underscores.
+Therefore, GFM ignores multiple underscores in words:
perform_complicated_task
@@ -124,7 +142,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multili
On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
GFM supports multiline blockquotes fenced by <code>>>></code>:
-```no-highlight
+```
>>>
If you paste a message from somewhere else
@@ -158,7 +176,7 @@ Blocks of code are either fenced by lines with three back-ticks <code>```</code>
or are indented with four spaces. Only the fenced code blocks support syntax
highlighting:
-```no-highlight
+```
Inline `code` has `back-ticks around` it.
```
@@ -248,21 +266,23 @@ However the wrapping tags cannot be mixed as such:
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
- Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
+```
+Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
- :zap: You can use emoji anywhere GFM is supported. :v:
+:zap: You can use emoji anywhere GFM is supported. :v:
- You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
- If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes.
- Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
+Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
- Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
+Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
- On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
- Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+```
Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you:
@@ -281,7 +301,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he
Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
-
### Special GitLab References
GFM recognizes special references.
@@ -343,7 +362,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-li
You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
-```no-highlight
+```
- [x] Completed task
- [ ] Incomplete task
- [ ] Sub-task 1
@@ -355,7 +374,7 @@ You can add task lists to issues, merge requests and comments. To create a task
Tasks formatted as ordered lists are supported as well:
-```no-highlight
+```
1. [x] Completed task
1. [ ] Incomplete task
1. [ ] Sub-task 1
@@ -412,7 +431,7 @@ This math is inline ![alt text](img/math_inline_sup_render_gfm.png).
This is on a separate line
-<div align="center"><img src="./img/math_inline_sup_render_gfm.png" ></div>
+<img src="./img/math_inline_sup_render_gfm.png" >
_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
@@ -440,7 +459,7 @@ Examples:
`HSL(540,70%,50%)`
`HSLA(540,70%,50%,0.7)`
-Become:
+Becomes:
![alt color-inline-colorchip-render-gfm](img/color_inline_colorchip_render_gfm.png)
@@ -482,7 +501,7 @@ For details see the [Mermaid official page][mermaid].
### Headers
-```no-highlight
+```
# H1
## H2
### H3
@@ -540,7 +559,7 @@ Note that the Emoji processing happens before the header IDs are generated, so t
Examples:
-```no-highlight
+```
Emphasis, aka italics, with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
@@ -550,7 +569,7 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
```
-Become:
+Becomes:
Emphasis, aka italics, with *asterisks* or _underscores_.
@@ -564,7 +583,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~
Examples:
-```no-highlight
+```
1. First ordered list item
2. Another item
* Unordered sub-list.
@@ -577,7 +596,7 @@ Examples:
+ Or pluses
```
-Become:
+Becomes:
1. First ordered list item
2. Another item
@@ -595,7 +614,7 @@ each subsequent paragraph should be indented to the same level as the start of t
Example:
-```no-highlight
+```
1. First ordered list item
Second paragraph of first item.
@@ -616,7 +635,7 @@ the paragraph will appear outside the list, instead of properly indented under t
Example:
-```no-highlight
+```
1. First ordered list item
Paragraph of first item.
@@ -676,7 +695,7 @@ Examples:
[logo]: img/markdown_logo.png
-Become:
+Becomes:
Here's our logo:
@@ -694,7 +713,7 @@ Reference-style:
Examples:
-```no-highlight
+```
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -703,7 +722,7 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
-Become:
+Becomes:
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
@@ -720,7 +739,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
Examples:
-```no-highlight
+```
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
@@ -730,7 +749,7 @@ Examples:
</dl>
```
-Become:
+Becomes:
<dl>
<dt>Definition list</dt>
@@ -788,7 +807,7 @@ ___
Underscores
```
-Become:
+Becomes:
Three or more...
@@ -826,7 +845,7 @@ This line is *on its own line*, because the previous line ends with two spaces.
spaces.
```
-Become:
+Becomes:
Here's a line for us to start with.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 48004471f0a..762d254d6cc 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -113,6 +113,14 @@ To add an existing Kubernetes cluster to your project:
After a couple of minutes, your cluster will be ready to go. You can now proceed
to install some [pre-defined applications](#installing-applications).
+To determine the:
+
+- API URL, run `kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'`.
+- Token:
+ 1. List the secrets by running: `kubectl get secrets`. Note the name of the secret you need the token for.
+ 1. Get the token for the appropriate secret by running: `kubectl get secret <SECRET_NAME> -o jsonpath="{['data']['token']}" | base64 -D`.
+- CA certificate, run `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`.
+
## Security implications
CAUTION: **Important:**
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
new file mode 100644
index 00000000000..3b81e439119
--- /dev/null
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -0,0 +1,49 @@
+# Runbooks
+
+Runbooks are a collection of documented procedures that explain how to
+carry out a particular process, be it starting, stopping, debugging,
+or troubleshooting a particular system.
+
+## Overview
+
+Historically, runbooks took the form of a decision tree or a detailed
+step-by-step guide depending on the condition or system.
+
+Modern implementations have introduced the concept of an "executable
+runbooks", where along with a well define process, operators can execute
+code blocks or database queries against a given environment.
+
+## Nurtch Executable Runbooks
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4.
+
+The JupyterHub app offered via GitLab’s Kubernetes integration now ships
+with Nurtch’s Rubix library, providing a simple way to create DevOps
+runbooks. A sample runbook is provided, showcasing common operations.
+
+**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE)
+for an overview of how this is acomplished in GitLab!**
+
+## Requirements
+
+To create an executable runbook, you will need:
+
+1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications.
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab).
+1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install
+ all the other applications. It is installed in its own pod inside the cluster which
+ can run the helm CLI in a safe environment.
+1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based
+ virtual hosting. It acts as a web proxy for your applications.
+1. **JupyterHub** - JupyterHub is a multi-user service for managing notebooks across
+ a team. Jupyter Notebooks provide a web-based interactive programming environment
+ used for data analysis, visualization, and machine learning.
+
+## Nurtch
+
+Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is
+an open-source python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks.
+Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified
+down to a couple of lines of code. Check the [Nurtch Documentation](http://docs.nurtch.com/en/latest)
+for more information.
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 4ea35a30bbf..2f5efbe84d9 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -1,6 +1,7 @@
# Migrating projects to a GitLab instance
-1. [From Bitbucket.org](bitbucket.md)
+1. [From Bitbucket Cloud (aka bitbucket.org)](bitbucket.md)
+1. [From Bitbucket Server (aka Stash)](bitbucket_server.md)
1. [From ClearCase](clearcase.md)
1. [From CVS](cvs.md)
1. [From FogBugz](fogbugz.md)
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index f5ea350a58f..9e2434c02ec 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -176,8 +176,8 @@ Clicking on the current board name in the upper left corner will reveal a
menu from where you can create another Issue Board and rename or delete the
existing one.
-Clicking on the main issue board link will take you to the last board
-you visited.
+When you're revisiting an issue board in a project or group with multiple boards,
+GitLab will automatically load the last board you visited.
NOTE: **Note:**
The Multiple Issue Boards feature is available for
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index f9ebf277125..0a7f7d37384 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -236,6 +236,35 @@ all your changes will be available to preview by anyone with the Review Apps lin
Find out about [bulk editing merge requests](../../project/bulk_editing.md).
+## Troubleshooting
+
+Sometimes things don't go as expected in a merge request, here are some
+troubleshooting steps.
+
+### Merge request cannot retrieve the pipeline status
+
+This can occur for one of two reasons:
+
+* Sidekiq doesn't pick up the changes fast enough
+* Because of the bug described in [#41545](https://gitlab.com/gitlab-org/gitlab-ce/issues/41545)
+
+#### Sidekiq
+
+Sidekiq didn't process the CI state change fast enough. Please wait a few
+seconds and the status will update automatically.
+
+#### Bug
+
+Merge Request pipeline statuses can't be retrieved when the following occurs:
+
+1. A Merge Requst is created
+1. The Merge Request is closed
+1. Changes are made in the project
+1. The Merge Request is reopened
+
+To enable the pipeline status to be properly retrieved, close and reopen the
+Merge Request again.
+
## Tips
Here are some tips that will help you be more efficient with merge requests in
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index e1eede8bbed..89b9621b8b9 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -212,7 +212,7 @@ security measure, necessary just for big companies, like banks and shoppings sit
with financial transactions.
Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group):
-> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](http://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
+> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._
Therefore, the reason why certificates are so important is that they encrypt
the connection between the **client** (you, me, your visitors)
diff --git a/doc/user/search/img/issues_filter_none_any.png b/doc/user/search/img/issues_filter_none_any.png
new file mode 100644
index 00000000000..9682fc55315
--- /dev/null
+++ b/doc/user/search/img/issues_filter_none_any.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 4f1b96b775c..3f9d07dacaa 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -40,6 +40,16 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+### Filtering by **None** / **Any**
+
+Some filter fields like milestone and assignee, allow you to filter by **None** or **Any**.
+
+![filter by none any](img/issues_filter_none_any.png)
+
+Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee.
+
+Selecting **Any** does the opposite. It returns results that have a non-empty value for that field.
+
### Searching for specific terms
You can filter issues and merge requests by specific terms included in titles or descriptions.
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 731c9209224..c590ac4b0ba 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -10,31 +10,32 @@ You can find notification settings under the user profile.
Notification settings are divided into three groups:
-* Global Settings
-* Group Settings
-* Project Settings
+- Global settings
+- Group settings
+- Project settings
Each of these settings have levels of notification:
-* Disabled - turns off notifications
-* Participating - receive notifications from related resources
-* Watch - receive notifications from projects or groups user is a member of
-* Global - notifications as set at the global settings
-* Custom - user will receive notifications when mentioned, is participant and custom selected events.
+- Watch: Receive notifications for any activity.
+- On Mention: Receive notifications when `@mentioned` in comments.
+- Participate: Receive notifications for threads you have participated in.
+- Disabled: Turns off notifications.
+- Custom: Receive notifications for custom selected events.
+- Global: For groups and projects, notifications as per global settings.
-#### Global Settings
+### Global Settings
-Global Settings are at the bottom of the hierarchy.
+Global settings are at the bottom of the hierarchy.
Any setting set here will be overridden by a setting at the group or a project level.
Group or Project settings can use `global` notification setting which will then use
anything that is set at Global Settings.
-#### Group Settings
+### Group Settings
![notification settings](img/notification_group_settings.png)
-Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
+Group settings are taking precedence over Global Settings but are on a level below Project or Subgroup settings:
```
Group < Subgroup < Project
@@ -46,11 +47,11 @@ Organization like this is suitable for users that belong to different groups but
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
-#### Project Settings
+### Project Settings
![notification settings](img/notification_project_settings.png)
-Project Settings are at the top level and any setting placed at this level will take precedence of any
+Project settings are at the top level and any setting placed at this level will take precedence of any
other setting.
This is suitable for users that have different needs for notifications per project basis.
These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
@@ -73,11 +74,12 @@ Below is the table of events users can be notified of:
### Issue / Merge request events
In most of the below cases, the notification will be sent to:
+
- Participants:
- the author and assignee of the issue/merge request
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- - anyone mentioned by `@username` in any of the comments on the issue/merge request
+ - anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
@@ -90,12 +92,16 @@ In most of the below cases, the notification will be sent to:
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
| Due issue | Participants and Custom notification level with this event selected |
+| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
| Close merge request | |
| Reopen merge request | |
| Merge merge request | |
+| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
@@ -129,16 +135,19 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
+
This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
+
- `own_activity`
- `assigned`
- `mentioned`
-The reason in this header will also be shown in the footer of the notification email. For example an email with the
+The reason in this header will also be shown in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
-**Note: Only reasons listed above have been implemented so far**
-Further implementation is [being discussed here](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062)
+NOTE: **Note:**
+Only reasons listed above have been implemented so far.
+Further implementation is [being discussed](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062).
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 4dd6b19e353..ae40b5f7557 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -65,6 +65,8 @@ module API
result
rescue Gitlab::GitAccess::UnauthorizedError => e
break response_with_status(code: 401, success: false, message: e.message)
+ rescue Gitlab::GitAccess::TimeoutError => e
+ break response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e
break response_with_status(code: 404, success: false, message: e.message)
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 405fc30a2ed..7909f9c7a00 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -8,6 +8,15 @@ module API
helpers ::Gitlab::IssuableMetadata
+ # EE::API::Issues would override the following helpers
+ helpers do
+ params :issues_params_ee do
+ end
+
+ params :issue_params_ee do
+ end
+ end
+
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def find_issues(args = {})
@@ -46,9 +55,11 @@ module API
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
use :pagination
+
+ use :issues_params_ee
end
- params :issue_params_ce do
+ params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
@@ -57,10 +68,8 @@ module API
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
- end
- params :issue_params do
- use :issue_params_ce
+ use :issue_params_ee
end
end
@@ -285,6 +294,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'List merge requests that are related to the issue' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ end
+ get ':id/issues/:issue_iid/related_merge_requests' do
+ issue = find_project_issue(params[:issue_iid])
+
+ merge_request_iids = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user)
+ .execute(issue)
+ .flatten
+ .map(&:iid)
+
+ merge_requests =
+ if merge_request_iids.present?
+ MergeRequestsFinder.new(current_user, project_id: user_project.id, iids: merge_request_iids).execute
+ else
+ MergeRequest.none
+ end
+
+ present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
+ end
+
desc 'List merge requests closing issue' do
success Entities::MergeRequestBasic
end
diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb
new file mode 100644
index 00000000000..53b5790bfa2
--- /dev/null
+++ b/lib/api/validations/types/safe_file.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# This module overrides the Grape type validator defined in
+# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb
+module API
+ module Validations
+ module Types
+ class SafeFile < ::Grape::Validations::Types::File
+ def value_coerced?(value)
+ super && value[:tempfile].is_a?(Tempfile)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 6e1d4eb335f..24746f4efc6 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -6,7 +6,7 @@ module API
def commit_params(attrs)
{
file_name: attrs[:file][:filename],
- file_content: File.read(attrs[:file][:tempfile]),
+ file_content: attrs[:file][:tempfile].read,
branch_name: attrs[:branch]
}
end
@@ -100,7 +100,7 @@ module API
success Entities::WikiAttachment
end
params do
- requires :file, type: File, desc: 'The attachment file to be uploaded'
+ requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index d7fe012883d..8e2358694d4 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -18,7 +18,7 @@ module Banzai
issuables = extractor.extract([doc])
issuables.each do |node, issuable|
- next if !can_read_cross_project? && issuable.project != project
+ next if !can_read_cross_project? && cross_reference?(issuable)
if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable)
node.content += " (#{issuable.state})"
@@ -31,7 +31,14 @@ module Banzai
private
def issuable_reference?(text, issuable)
- text == issuable.reference_link_text(project || group)
+ CGI.unescapeHTML(text) == issuable.reference_link_text(project || group)
+ end
+
+ def cross_reference?(issuable)
+ return true if issuable.project != project
+ return true if issuable.respond_to?(:group) && issuable.group != group
+
+ false
end
def can_read_cross_project?
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 0a05d46db4c..341dbb74fe0 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -9,13 +9,11 @@ module Banzai
# so we can avoid N+1 queries problem
class IssuableExtractor
- QUERY = %q(
- descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
- [@data-reference-type="issue" or @data-reference-type="merge_request"]
- ).freeze
-
attr_reader :context
+ ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze
+ MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze
+
# context - An instance of Banzai::RenderContext.
def initialize(context)
@context = context
@@ -24,21 +22,38 @@ module Banzai
# Returns Hash in the form { node => issuable_instance }
def extract(documents)
nodes = documents.flat_map do |document|
- document.xpath(QUERY)
+ document.xpath(query)
end
- issue_parser = Banzai::ReferenceParser::IssueParser.new(context)
+ # The project or group for the issuable might be pending for deletion!
+ # Filter them out because we don't care about them.
+ issuables_for_nodes(nodes).select { |node, issuable| issuable.project || issuable.group }
+ end
+
+ private
- merge_request_parser =
+ def issuables_for_nodes(nodes)
+ parsers.each_with_object({}) do |parser, result|
+ result.merge!(parser.records_for_nodes(nodes))
+ end
+ end
+
+ def parsers
+ [
+ Banzai::ReferenceParser::IssueParser.new(context),
Banzai::ReferenceParser::MergeRequestParser.new(context)
+ ]
+ end
- issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge(
- merge_request_parser.records_for_nodes(nodes)
+ def query
+ %Q(
+ descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")]
+ [#{reference_types.join(' or ')}]
)
+ end
- # The project for the issue/MR might be pending for deletion!
- # Filter them out because we don't care about them.
- issuables_for_nodes.select { |node, issuable| issuable.project }
+ def reference_types
+ [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE]
end
end
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index a340a276640..655278da711 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -153,7 +153,7 @@ module ExtractsPath
private
- # overriden in subclasses, do not remove
+ # overridden in subclasses, do not remove
def get_id
id = [params[:id] || params[:ref]]
id << "/" + params[:path] unless params[:path].blank?
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index d2029a141e7..6eb5f9e2300 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -151,17 +151,15 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def personal_access_token_check(password)
return unless password.present?
- token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
+ token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end
end
- # rubocop: enable CodeReuse/ActiveRecord
def valid_oauth_token?(token)
token && token.accessible? && valid_scoped_token?(token, [:api])
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
index 4a5f9d2839d..36fc8061d92 100644
--- a/lib/gitlab/auth/o_auth/auth_hash.rb
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -80,7 +80,7 @@ module Gitlab
end
# Get the first part of the email address (before @)
- # In addtion in removes illegal characters
+ # In addition in removes illegal characters
def generate_username(email)
email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 5df6db6f366..c304adc64db 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -73,7 +73,6 @@ module Gitlab
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def find_personal_access_token
token =
current_request.params[PRIVATE_TOKEN_PARAM].presence ||
@@ -82,9 +81,8 @@ module Gitlab
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
- PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError)
+ PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
end
- # rubocop: enable CodeReuse/ActiveRecord
def find_oauth_access_token
token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
diff --git a/lib/gitlab/background_migration/digest_column.rb b/lib/gitlab/background_migration/digest_column.rb
new file mode 100644
index 00000000000..22a3bb8f8f3
--- /dev/null
+++ b/lib/gitlab/background_migration/digest_column.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/Documentation
+module Gitlab
+ module BackgroundMigration
+ class DigestColumn
+ class PersonalAccessToken < ActiveRecord::Base
+ self.table_name = 'personal_access_tokens'
+ end
+
+ def perform(model, attribute_from, attribute_to, start_id, stop_id)
+ model = model.constantize if model.is_a?(String)
+
+ model.transaction do
+ relation = model.where(id: start_id..stop_id).where.not(attribute_from => nil).lock
+
+ relation.each do |instance|
+ instance.update_columns(attribute_to => Gitlab::CryptoHelper.sha256(instance.read_attribute(attribute_from)),
+ attribute_from => nil)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/redact_links.rb b/lib/gitlab/background_migration/redact_links.rb
new file mode 100644
index 00000000000..f5d3bcdd517
--- /dev/null
+++ b/lib/gitlab/background_migration/redact_links.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RedactLinks
+ module Redactable
+ extend ActiveSupport::Concern
+
+ def redact_field!(field)
+ self[field].gsub!(%r{/sent_notifications/\h{32}/unsubscribe}, '/sent_notifications/REDACTED/unsubscribe')
+
+ if self.changed?
+ self.update_columns(field => self[field],
+ "#{field}_html" => nil)
+ end
+ end
+ end
+
+ class Note < ActiveRecord::Base
+ include EachBatch
+ include Redactable
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+ include Redactable
+
+ self.table_name = 'issues'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+ include Redactable
+
+ self.table_name = 'merge_requests'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Snippet < ActiveRecord::Base
+ include EachBatch
+ include Redactable
+
+ self.table_name = 'snippets'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def perform(model_name, field, start_id, stop_id)
+ link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%"
+ model = "Gitlab::BackgroundMigration::RedactLinks::#{model_name}".constantize
+
+ model.where("#{field} like ?", link_pattern).where(id: start_id..stop_id).each do |resource|
+ resource.redact_field!(field)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
index e5e8837221e..bc434b0cb64 100644
--- a/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_services.rb
@@ -3,8 +3,8 @@
module Gitlab
module BackgroundMigration
- # Ensures services which previously recieved all notes events continue
- # to recieve confidential ones.
+ # Ensures services which previously received all notes events continue
+ # to receive confidential ones.
class SetConfidentialNoteEventsOnServices
class Service < ActiveRecord::Base
self.table_name = 'services'
diff --git a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
index 171c8ef21b7..28d8d2c640b 100644
--- a/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
+++ b/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks.rb
@@ -3,8 +3,8 @@
module Gitlab
module BackgroundMigration
- # Ensures hooks which previously recieved all notes events continue
- # to recieve confidential ones.
+ # Ensures hooks which previously received all notes events continue
+ # to receive confidential ones.
class SetConfidentialNoteEventsOnWebhooks
class WebHook < ActiveRecord::Base
self.table_name = 'web_hooks'
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
index 0d79594363e..f1a653a9d95 100644
--- a/lib/gitlab/blame.rb
+++ b/lib/gitlab/blame.rb
@@ -43,8 +43,7 @@ module Gitlab
def highlighted_lines
@blob.load_all_data!
- @highlighted_lines ||=
- Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines
+ @highlighted_lines ||= @blob.present.highlight.lines
end
def project
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
index b369b9e7600..dfbb83f7bb9 100644
--- a/lib/gitlab/cache/ci/project_pipeline_status.rb
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -42,7 +42,7 @@ module Gitlab
end
def self.cache_key_for_project(project)
- "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status"
+ "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status/#{project.commit&.sha}"
end
def self.update_for_pipeline(pipeline)
@@ -84,9 +84,7 @@ module Gitlab
def load_from_project
return unless commit
- self.sha = commit.sha
- self.status = commit.status
- self.ref = project.default_branch
+ self.sha, self.status, self.ref = commit.sha, commit.status, project.default_branch
end
# We only cache the status for the HEAD commit of a project
@@ -104,6 +102,8 @@ module Gitlab
def load_from_cache
Gitlab::Redis::Cache.with do |redis|
self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
+
+ self.status = nil if self.status.empty?
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 49e7f7e1fd7..074afe9c412 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -18,11 +18,24 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
- attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
+ LOG_MESSAGES = {
+ push_checks: "Checking if you are allowed to push...",
+ delete_default_branch_check: "Checking if default branch is being deleted...",
+ protected_branch_checks: "Checking if you are force pushing to a protected branch...",
+ protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...",
+ protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...",
+ tag_checks: "Checking if you are allowed to change existing tags...",
+ protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...",
+ lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...",
+ commits_check_file_paths_validation: "Validating commits' file paths...",
+ commits_check: "Validating commit contents..."
+ }.freeze
+
+ attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger
def initialize(
change, user_access:, project:, skip_authorization: false,
- skip_lfs_integrity_check: false, protocol:
+ skip_lfs_integrity_check: false, protocol:, logger:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@@ -32,6 +45,9 @@ module Gitlab
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
+
+ @logger = logger
+ @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
end
def exec(skip_commits_check: false)
@@ -49,26 +65,32 @@ module Gitlab
protected
def push_checks
- unless can_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless can_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
+ end
end
end
def branch_checks
return unless branch_name
- if deletion? && branch_name == project.default_branch
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do
+ if deletion? && branch_name == project.default_branch
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
+ end
end
protected_branch_checks
end
def protected_branch_checks
- return unless ProtectedBranch.protected?(project, branch_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- if forced_push?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ if forced_push?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
+ end
end
if deletion?
@@ -79,23 +101,27 @@ module Gitlab
end
def protected_branch_deletion_checks
- unless user_access.can_delete_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
- end
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ unless user_access.can_delete_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
+ end
- unless updated_from_web?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ unless updated_from_web?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
+ end
end
end
def protected_branch_push_checks
- if matching_merge_request?
- unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
- end
- else
- unless user_access.can_push_to_branch?(branch_name)
- raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if matching_merge_request?
+ unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
+ end
+ else
+ unless user_access.can_push_to_branch?(branch_name)
+ raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message
+ end
end
end
end
@@ -103,21 +129,25 @@ module Gitlab
def tag_checks
return unless tag_name
- if tag_exists? && user_access.cannot_do_action?(:admin_project)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ if tag_exists? && user_access.cannot_do_action?(:admin_project)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
+ end
end
protected_tag_checks
end
def protected_tag_checks
- return unless ProtectedTag.protected?(project, tag_name)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
- raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
+ raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
- unless user_access.can_create_tag?(tag_name)
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ unless user_access.can_create_tag?(tag_name)
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
+ end
end
end
@@ -125,14 +155,20 @@ module Gitlab
return if deletion? || newrev.nil?
return unless should_run_commit_validations?
- # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- commits.each do |commit|
- commit_check.validate(commit, validations_for_commit(commit))
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits.each do |commit|
+ logger.check_timeout_reached
+
+ commit_check.validate(commit, validations_for_commit(commit))
+ end
end
end
- commit_check.validate_file_paths
+ logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do
+ commit_check.validate_file_paths
+ end
end
# Method overwritten in EE to inject custom validations
@@ -194,10 +230,12 @@ module Gitlab
end
def lfs_objects_exist_check
- lfs_check = Checks::LfsIntegrity.new(project, newrev)
+ logger.log_timed(LOG_MESSAGES[__method__]) do
+ lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left)
- if lfs_check.objects_missing?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ if lfs_check.objects_missing?
+ raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing]
+ end
end
end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index fa3dc1808df..1652d5a30a4 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -3,9 +3,10 @@
module Gitlab
module Checks
class LfsIntegrity
- def initialize(project, newrev)
+ def initialize(project, newrev, time_left)
@project = project
@newrev = newrev
+ @time_left = time_left
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -13,7 +14,7 @@ module Gitlab
return false unless @newrev && @project.lfs_enabled?
new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev)
- .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT)
+ .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left)
return false unless new_lfs_pointers.present?
diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb
new file mode 100644
index 00000000000..f365e0a43f6
--- /dev/null
+++ b/lib/gitlab/checks/timed_logger.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Checks
+ class TimedLogger
+ TimeoutError = Class.new(StandardError)
+
+ attr_reader :start_time, :header, :log, :timeout
+
+ def initialize(start_time: Time.now, log: [], header: "", timeout:)
+ @start_time = start_time
+ @timeout = timeout
+ @header = header
+ @log = log
+ end
+
+ # Adds trace of method being tracked with
+ # the correspondent time it took to run it.
+ # We make use of the start default argument
+ # on unit tests related to this method
+ #
+ def log_timed(log_message, start = Time.now)
+ check_timeout_reached
+
+ timed = true
+
+ yield
+
+ append_message(log_message + time_suffix_message(start: start))
+ rescue GRPC::DeadlineExceeded, TimeoutError
+ args = { cancelled: true }
+ args[:start] = start if timed
+
+ append_message(log_message + time_suffix_message(args))
+
+ raise TimeoutError
+ end
+
+ def check_timeout_reached
+ return unless time_expired?
+
+ raise TimeoutError
+ end
+
+ def time_left
+ (start_time + timeout.seconds) - Time.now
+ end
+
+ def full_message
+ header + log.join("\n")
+ end
+
+ # We always want to append in-place on the log
+ def append_message(message)
+ log << message
+ end
+
+ private
+
+ def time_expired?
+ time_left <= 0
+ end
+
+ def time_suffix_message(cancelled: false, start: nil)
+ return " (#{elapsed_time(start)}ms)" unless cancelled
+
+ if start
+ " (cancelled after #{elapsed_time(start)}ms)"
+ else
+ " (cancelled)"
+ end
+ end
+
+ def elapsed_time(start)
+ to_ms(Time.now - start)
+ end
+
+ def to_ms(elapsed)
+ (elapsed.to_f * 1000).round(2)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index e780f8c646b..974b5ad6877 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# ANSI color library
#
# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
@@ -265,7 +267,7 @@ module Gitlab
def reset_state
@offset = 0
@n_open_tags = 0
- @out = ''
+ @out = +''
reset
end
diff --git a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
index ee3647f24fd..25a82086676 100644
--- a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
+++ b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
index fa6842cf36a..cf37d700991 100644
--- a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
+++ b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 551d4f4473e..08dac756cc1 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'zlib'
require 'json'
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 85072a072d6..d0a80518ae8 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/artifacts/path.rb b/lib/gitlab/ci/build/artifacts/path.rb
index 9cd9b36c5f8..65cd935afaa 100644
--- a/lib/gitlab/ci/build/artifacts/path.rb
+++ b/lib/gitlab/ci/build/artifacts/path.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb
index 29a7a27c963..58adf6e506d 100644
--- a/lib/gitlab/ci/build/credentials/base.rb
+++ b/lib/gitlab/ci/build/credentials/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb
index 2423aa8857d..fa805abb8bb 100644
--- a/lib/gitlab/ci/build/credentials/factory.rb
+++ b/lib/gitlab/ci/build/credentials/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb
index 55eafcaed10..1c8588d9913 100644
--- a/lib/gitlab/ci/build/credentials/registry.rb
+++ b/lib/gitlab/ci/build/credentials/registry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index c811f88f483..4dd932f61d4 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb
index d10cc7802d4..43c46ad74af 100644
--- a/lib/gitlab/ci/build/policy.rb
+++ b/lib/gitlab/ci/build/policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index 782f6c4c0af..4c7dc947cd0 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index 4aa5dc89f47..10934536536 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index f09ba42c074..ceb5210cfb5 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 9d2a362b7d4..0698136166a 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 0b1ebe4e048..d587c896712 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Build
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
index 7b7354bce16..a4f01468e8e 100644
--- a/lib/gitlab/ci/charts.rb
+++ b/lib/gitlab/ci/charts.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Charts
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index fedaf18ef30..2fb3c4582e7 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
#
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index e80f9d2e452..ef5f25b42c0 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
index 3e87a09704e..3c2e1df9b83 100644
--- a/lib/gitlab/ci/config/entry/attributable.rb
+++ b/lib/gitlab/ci/config/entry/attributable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
index f3357f85b99..b9639c83075 100644
--- a/lib/gitlab/ci/config/entry/boolean.rb
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index d7e09acbbf3..0a25057f482 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index 9f66f11be9b..d9658291ebe 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 697f622c45e..4aabf0cfa31 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
index 12a063059cb..690409ccf77 100644
--- a/lib/gitlab/ci/config/entry/coverage.rb
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 0c1f9eb7cbf..07e9e1d3f67 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 6be8288748f..85c9c3511a4 100644
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index 04077fa7a61..eba203d9d06 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
index 6fc3aa385bc..dc0ede2a25f 100644
--- a/lib/gitlab/ci/config/entry/hidden.rb
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 2844be80a84..fc453b72fa5 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index f290ff3a565..e4610faa327 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 96b6f2e5d6c..1535b108000 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index f27ad0a7759..963b200c7bb 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index a3d4432be82..4043629dea9 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index 26505c91be3..347089722e4 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
index 68dad161149..9580b5e2e7f 100644
--- a/lib/gitlab/ci/config/entry/paths.rb
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index c92562f8c85..0535d7c1a1a 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index 29ecd9995ca..f7d39e5cf55 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 3e2ebcff31a..47bf9205147 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index 0066894e069..bdf7f80f382 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb
index 12764629686..9961bbfaa40 100644
--- a/lib/gitlab/ci/config/entry/simplifiable.rb
+++ b/lib/gitlab/ci/config/entry/simplifiable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
index b7afaba1de8..65ab5953131 100644
--- a/lib/gitlab/ci/config/entry/stage.rb
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index ec187bd3732..ab184246d29 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index 1171ac10f22..77dcfa88170 100644
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
index fbb2551e870..bab32489d2f 100644
--- a/lib/gitlab/ci/config/entry/unspecified.rb
+++ b/lib/gitlab/ci/config/entry/unspecified.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
index e45787773a8..08a6593c980 100644
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
index 2df23a3edcd..33ffdd3a95d 100644
--- a/lib/gitlab/ci/config/entry/validator.rb
+++ b/lib/gitlab/ci/config/entry/validator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index f6b4ba7843e..805d26ca8d8 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index 8acab605c91..6fd3cec2f5f 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb
index 141d2714cb6..b4c491e84a6 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/ci/config/loader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Config
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 73f36735e35..b1db9084662 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class CronParser
diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb
index 0daddaa638c..58d55b1bd6f 100644
--- a/lib/gitlab/ci/mask_secret.rb
+++ b/lib/gitlab/ci/mask_secret.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci::MaskSecret
class << self
def mask!(value, token)
return value unless value.present? && token.present?
+ # We assume 'value' must be mutable, given
+ # that frozen string is enabled.
value.gsub!(token, 'x' * token.length)
value
end
diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb
index 3994a50772b..fbdb84c0522 100644
--- a/lib/gitlab/ci/model.rb
+++ b/lib/gitlab/ci/model.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Model
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 5d7d9a751d8..ed5a79d9b9b 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -14,10 +14,10 @@ module Gitlab
test_case = create_test_case(test_case)
test_suite.add_test_case(test_case)
end
- rescue REXML::ParseException => e
- raise JunitParserError, "XML parsing failed: #{e.message}"
- rescue => e
- raise JunitParserError, "JUnit parsing failed: #{e.message}"
+ rescue REXML::ParseException
+ raise JunitParserError, "XML parsing failed"
+ rescue
+ raise JunitParserError, "JUnit parsing failed"
end
private
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index efed19da21c..bab1c73e2f1 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index b5eb0cfa2f0..b445a872b3d 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index a53c80d34f7..05978804d92 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -1,4 +1,7 @@
-module Gitlab # rubocop:disable Naming/FileName
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Gitlab
module Ci
module Pipeline
module Chain
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index 02493c7fe02..c882241ef6a 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index bf1380a1da9..6bb3a75291b 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index f34c11ca3c2..633d3cd4f6b 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
index e24630656d3..99780409085 100644
--- a/lib/gitlab/ci/pipeline/chain/sequence.rb
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 32cbb7ca6af..b9707d2f8f5 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index 13c6fedd831..ebd7e6e8289 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
index a3bd2a5a23a..28c38cc3d18 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
index 9699c24e5b6..d88851d8245 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb
index 30701e1de1b..de24bbf688b 100644
--- a/lib/gitlab/ci/pipeline/duration.rb
+++ b/lib/gitlab/ci/pipeline/duration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb
index f57df7c5637..61d392121d8 100644
--- a/lib/gitlab/ci/pipeline/expression.rb
+++ b/lib/gitlab/ci/pipeline/expression.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
index 047ab66e9b3..70c774416f6 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
index 3a2f0c6924e..668e85f5b9e 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 10957598f76..cd17bc4d78b 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
index a2778716924..be7258c201a 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
index f640d0b5855..3ebceb92eb7 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
index 9b239c29ea4..d7e6dacf068 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
index 346c92dc51e..2db2bf011f1 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
index f2611d65faf..ef9ddb6cae9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
index 37643c8ef53..85c0899e4f6 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index 4cacb1e62c9..f26542361a2 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
index 90f94d0b763..ed184309ab4 100644
--- a/lib/gitlab/ci/pipeline/expression/parser.rb
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index b36f1e0f865..b03611f756e 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/expression/token.rb b/lib/gitlab/ci/pipeline/expression/token.rb
index 58211800b88..513d43f6fca 100644
--- a/lib/gitlab/ci/pipeline/expression/token.rb
+++ b/lib/gitlab/ci/pipeline/expression/token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb
index db9706924bb..1fd3a61017f 100644
--- a/lib/gitlab/ci/pipeline/seed/base.rb
+++ b/lib/gitlab/ci/pipeline/seed/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 6980b0b7aff..ef738a93bfe 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 2b58d9863a0..4775ff15581 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Pipeline
diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb
index b4d08ed257f..292e273a03a 100644
--- a/lib/gitlab/ci/reports/test_case.rb
+++ b/lib/gitlab/ci/reports/test_case.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_reports.rb
index c87bdb4a8a2..7397ff35d46 100644
--- a/lib/gitlab/ci/reports/test_reports.rb
+++ b/lib/gitlab/ci/reports/test_reports.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb
index 726c6a11a81..11810bdc0a8 100644
--- a/lib/gitlab/ci/reports/test_reports_comparer.rb
+++ b/lib/gitlab/ci/reports/test_reports_comparer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb
index b5f15397c0f..b0391160c15 100644
--- a/lib/gitlab/ci/reports/test_suite.rb
+++ b/lib/gitlab/ci/reports/test_suite.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb
index 642aa593092..9cb7db5934c 100644
--- a/lib/gitlab/ci/reports/test_suite_comparer.rb
+++ b/lib/gitlab/ci/reports/test_suite_comparer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Reports
diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb
index 6c9125647ad..45d9ba41e92 100644
--- a/lib/gitlab/ci/status/build/action.rb
+++ b/lib/gitlab/ci/status/build/action.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 024047d4983..43fb5cdbbe6 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/canceled.rb b/lib/gitlab/ci/status/build/canceled.rb
index c83e2734a73..0518b9e673d 100644
--- a/lib/gitlab/ci/status/build/canceled.rb
+++ b/lib/gitlab/ci/status/build/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
index c1fc70ac266..6a75ec5c37f 100644
--- a/lib/gitlab/ci/status/build/common.rb
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/created.rb b/lib/gitlab/ci/status/build/created.rb
index 5be8e9de425..780fea23123 100644
--- a/lib/gitlab/ci/status/build/created.rb
+++ b/lib/gitlab/ci/status/build/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/erased.rb b/lib/gitlab/ci/status/build/erased.rb
index 495227c2ffb..d74cfc1ee77 100644
--- a/lib/gitlab/ci/status/build/erased.rb
+++ b/lib/gitlab/ci/status/build/erased.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 4a74d6d6ed1..6e4bfe23f2b 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 50b0d044265..7cc1cc6b8e3 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
@@ -11,7 +13,8 @@ module Gitlab
runner_system_failure: 'runner system failure',
missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner',
- stale_schedule: 'stale schedule'
+ stale_schedule: 'stale schedule',
+ job_execution_timeout: 'job execution timeout'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index ca0046fb1f7..d7570fdd3e2 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb
index 042da6392d3..d01b09f1398 100644
--- a/lib/gitlab/ci/status/build/manual.rb
+++ b/lib/gitlab/ci/status/build/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/pending.rb b/lib/gitlab/ci/status/build/pending.rb
index 9dd9a27ad57..95f668295dd 100644
--- a/lib/gitlab/ci/status/build/pending.rb
+++ b/lib/gitlab/ci/status/build/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index a8b9ebf0803..c66b8ca5654 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retried.rb b/lib/gitlab/ci/status/build/retried.rb
index 6e190e4ee3c..b489dc68733 100644
--- a/lib/gitlab/ci/status/build/retried.rb
+++ b/lib/gitlab/ci/status/build/retried.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 5aeb8e51480..eb6b3f21604 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/scheduled.rb b/lib/gitlab/ci/status/build/scheduled.rb
index 62ad9083616..f443dbee120 100644
--- a/lib/gitlab/ci/status/build/scheduled.rb
+++ b/lib/gitlab/ci/status/build/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/skipped.rb b/lib/gitlab/ci/status/build/skipped.rb
index 3e678d0baee..4fe2f7b3114 100644
--- a/lib/gitlab/ci/status/build/skipped.rb
+++ b/lib/gitlab/ci/status/build/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index dea838bfa39..a620e7ad126 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/build/unschedule.rb b/lib/gitlab/ci/status/build/unschedule.rb
index e1b7b83428c..9110839cb55 100644
--- a/lib/gitlab/ci/status/build/unschedule.rb
+++ b/lib/gitlab/ci/status/build/unschedule.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index e6195a60d4f..07f37732023 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 9d6a2f51c11..ea773ee9944 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 846f00b83dd..fface4bb97b 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/extended.rb b/lib/gitlab/ci/status/extended.rb
index 1e8101f8949..b72a28ed0b6 100644
--- a/lib/gitlab/ci/status/extended.rb
+++ b/lib/gitlab/ci/status/extended.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
index 9307545b5b1..4169f5b3210 100644
--- a/lib/gitlab/ci/status/external/common.rb
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
index 07b15bd8d97..91fafb940a8 100644
--- a/lib/gitlab/ci/status/external/factory.rb
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index 15836c699c7..3446644eff8 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index 27ce85bd3ed..770ed7d4d5a 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/common.rb b/lib/gitlab/ci/status/group/common.rb
index cfd4329a923..0b5ea0712ca 100644
--- a/lib/gitlab/ci/status/group/common.rb
+++ b/lib/gitlab/ci/status/group/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/group/factory.rb b/lib/gitlab/ci/status/group/factory.rb
index d118116cfc3..ee785856fdd 100644
--- a/lib/gitlab/ci/status/group/factory.rb
+++ b/lib/gitlab/ci/status/group/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index fc387e2fd25..50c92add400 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index 6780780db32..cea7e6ed938 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb
index bf7e484ee9b..ed13a439be0 100644
--- a/lib/gitlab/ci/status/pipeline/blocked.rb
+++ b/lib/gitlab/ci/status/pipeline/blocked.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/common.rb b/lib/gitlab/ci/status/pipeline/common.rb
index 61bb07beb0f..7b34a2ea858 100644
--- a/lib/gitlab/ci/status/pipeline/common.rb
+++ b/lib/gitlab/ci/status/pipeline/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/delayed.rb b/lib/gitlab/ci/status/pipeline/delayed.rb
index 12736861c89..e61acdcd167 100644
--- a/lib/gitlab/ci/status/pipeline/delayed.rb
+++ b/lib/gitlab/ci/status/pipeline/delayed.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 0adf83fa197..5d1a8bbd924 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index ee13905e46d..ac7dd74cdce 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index 3adcfa36af2..16ad1da89e3 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 0dbdc4de426..aaec1e1d201 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb
index f60a7662075..f12daaa9676 100644
--- a/lib/gitlab/ci/status/stage/common.rb
+++ b/lib/gitlab/ci/status/stage/common.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 4c37f084d07..58f4642510b 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index 731013ec017..020f2c5b89f 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 32b4cf43e48..6632cd9b143 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Status
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 6fa59e41d20..5a8cd92b75f 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -116,12 +116,9 @@ code_quality:
license_management:
stage: test
- image: docker:stable
+ image: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
allow_failure: true
- services:
- - docker:stable-dind
script:
- - setup_docker
- license_management
artifacts:
paths: [gl-license-management-report.json]
@@ -210,7 +207,7 @@ container_scanning:
refs:
- branches
variables:
- - $GITLAB_FEATURES =~ /\bsast_container\b/
+ - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/
except:
variables:
- $CONTAINER_SCANNING_DISABLED
@@ -525,11 +522,7 @@ rollout 100%:
}
function license_management() {
- # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable"
- LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
-
- docker run --volume "$PWD:/code" \
- "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code
+ /run.sh analyze .
}
function sast() {
@@ -823,7 +816,7 @@ rollout 100%:
function initialize_tiller() {
echo "Checking Tiller..."
- export HELM_HOST=":44134"
+ export HELM_HOST="localhost:44134"
tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 &
echo "Tiller is listening on ${HELM_HOST}"
diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
index d61ff239e13..492b3d03db2 100644
--- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
@@ -15,7 +15,7 @@
# * Publishes the documentation for `master` branch.
variables:
- # This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
+ # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 93e219a21f9..8eccd262db9 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
index 2147f62a84a..e9b3199d56e 100644
--- a/lib/gitlab/ci/trace/chunked_io.rb
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
##
# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
# source: https://gitlab.com/snippets/1685610
@@ -66,8 +68,8 @@ module Gitlab
end
end
- def read(length = nil, outbuf = "")
- out = ""
+ def read(length = nil, outbuf = nil)
+ out = []
length ||= size - tell
@@ -83,17 +85,18 @@ module Gitlab
length -= chunk_data.bytesize
end
+ out = out.join
+
# If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
if outbuf
- outbuf.slice!(0, outbuf.bytesize)
- outbuf << out
+ outbuf.replace(out)
end
out
end
def readline
- out = ""
+ out = []
until eof?
data = chunk_slice_from_offset
@@ -109,7 +112,7 @@ module Gitlab
end
end
- out
+ out.join
end
def write(data)
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
index c09089d6475..f33f8cc56c1 100644
--- a/lib/gitlab/ci/trace/section_parser.rb
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index a71040e5e56..bd40fdf59b1 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class Trace
@@ -129,8 +131,7 @@ module Gitlab
debris = ''
until (buf = read_backward(BUFFER_SIZE)).empty?
- buf += debris
- debris, *lines = buf.each_line.to_a
+ debris, *lines = (buf + debris).each_line.to_a
lines.reverse_each do |line|
yield(line.force_encoding(Encoding.default_external))
end
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index ad30b3f427c..a7b4e0348c2 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 7da6d09d440..fdf852e8788 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
module Variables
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index a427aa30683..39a1b52e531 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Gitlab
module Ci
class YamlProcessor
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
index 331c39f7d6b..4ed9a9a02ab 100644
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -11,7 +11,11 @@ module Gitlab
# Importantly RAM is for _all_workers (ie, the cluster),
# not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX
worker_count = puma_options[:workers] || 1
- config.ram = worker_count * puma_per_worker_max_memory_mb
+ # The Puma Worker Killer checks the total RAM used by both the master
+ # and worker processes. Bump the limits to N+1 instead of N workers
+ # to account for this:
+ # https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57
+ config.ram = (worker_count + 1) * puma_per_worker_max_memory_mb
config.frequency = 20 # seconds
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index 30911b49b18..501c2111530 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -3,6 +3,7 @@ module Gitlab
class File
include Gitlab::Routing
include IconsHelper
+ include Gitlab::Utils::StrongMemoize
CONTEXT_LINES = 3
@@ -30,11 +31,8 @@ module Gitlab
end
def highlight_lines!
- their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
- our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
-
- their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
- our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
+ their_highlight = Gitlab::Highlight.highlight(their_path, their_lines, language: their_language).lines
+ our_highlight = Gitlab::Highlight.highlight(our_path, our_lines, language: our_language).lines
lines.each do |line|
line.rich_text =
@@ -182,6 +180,34 @@ module Gitlab
raw_line[:line_new], parent_file: self)
end
end
+
+ def their_language
+ strong_memoize(:their_language) do
+ repository.gitattribute(their_path, 'gitlab-language')
+ end
+ end
+
+ def our_language
+ strong_memoize(:our_language) do
+ if our_path == their_path
+ their_language
+ else
+ repository.gitattribute(our_path, 'gitlab-language')
+ end
+ end
+ end
+
+ def their_lines
+ strong_memoize(:their_lines) do
+ lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+ end
+ end
+
+ def our_lines
+ strong_memoize(:our_lines) do
+ lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+ end
+ end
end
end
end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index c819bffdfd6..5ed6427072a 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -73,7 +73,7 @@ module Gitlab
# re-running the contributed projects query in each union is expensive, so
# use IN(project_ids...) instead. It's the intersection of two users so
# the list will be (relatively) short
- @contributed_project_ids ||= projects.uniq.pluck(:id)
+ @contributed_project_ids ||= projects.distinct.pluck(:id)
authed_projects = Project.where(id: @contributed_project_ids)
.with_feature_available_for_user(feature, current_user)
.reorder(nil)
diff --git a/lib/gitlab/crypto_helper.rb b/lib/gitlab/crypto_helper.rb
new file mode 100644
index 00000000000..68d0b5d8f8a
--- /dev/null
+++ b/lib/gitlab/crypto_helper.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CryptoHelper
+ extend self
+
+ AES256_GCM_OPTIONS = {
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ iv: Settings.attr_encrypted_db_key_base_truncated[0..11]
+ }.freeze
+
+ def sha256(value)
+ salt = Settings.attr_encrypted_db_key_base_truncated
+ ::Digest::SHA256.base64digest("#{value}#{salt}")
+ end
+
+ def aes256_gcm_encrypt(value)
+ encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value))
+ Base64.encode64(encrypted_token)
+ end
+
+ def aes256_gcm_decrypt(value)
+ return unless value
+
+ encrypted_token = Base64.decode64(value)
+ Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token))
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index a17f27a3147..f98d6dbd46f 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -879,7 +879,7 @@ module Gitlab
columns(table).find { |column| column.name == name }
end
- # This will replace the first occurance of a string in a column with
+ # This will replace the first occurrence of a string in a column with
# the replacement
# On postgresql we can use `regexp_replace` for that.
# On mysql we find the location of the pattern, and overwrite it
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index b79ff771a2b..2ad6fe8449d 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -17,7 +17,6 @@ module Gitlab
@diffable = diffable
@include_stats = diff_options.delete(:include_stats)
- @diffs = diffable.raw_diffs(diff_options)
@project = project
@diff_options = diff_options
@diff_refs = diff_refs
@@ -25,8 +24,12 @@ module Gitlab
@repository = project.repository
end
+ def diffs
+ @diffs ||= diffable.raw_diffs(diff_options)
+ end
+
def diff_files
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ @diff_files ||= diffs.decorate! { |diff| decorate_diff!(diff) }
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index a605ddb5c33..1d833183ec3 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -79,7 +79,7 @@ module Gitlab
return [] unless blob
blob.load_all_data!
- Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines
+ blob.present.highlight.lines
end
end
end
diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb
index b68a1636814..8457e0c4cb6 100644
--- a/lib/gitlab/diff/position_tracer.rb
+++ b/lib/gitlab/diff/position_tracer.rb
@@ -24,7 +24,7 @@ module Gitlab
# head of `feature` was commit B, resulting in the original diff A->B.
# Since creation, `master` was updated to C.
# Now `feature` is being updated to D, and the newly generated MR diff is C->D.
- # It is possible that C and D are direct decendants of A and B respectively,
+ # It is possible that C and D are direct descendants of A and B respectively,
# but this isn't necessarily the case as rebases and merges come into play.
#
# Suppose we have a diff note on the original diff A->B. Now that the MR
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 13b0bb930f4..0bd1d3420a2 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -5,6 +5,7 @@ module Gitlab
class Blob
include Gitlab::BlobHelper
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
# This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection and LFS
@@ -75,7 +76,7 @@ module Gitlab
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
def batch_lfs_pointers(repository, blob_ids)
- repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a)
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 74cdabfed9d..2820491b65d 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -3,6 +3,7 @@ module Gitlab
module Git
class Commit
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :raw_commit, :head
@@ -59,7 +60,7 @@ module Gitlab
# This saves us an RPC round trip.
return nil if commit_id.include?(':')
- commit = repo.wrapped_gitaly_errors do
+ commit = wrapped_gitaly_errors do
repo.gitaly_commit_client.find_commit(commit_id)
end
@@ -100,7 +101,7 @@ module Gitlab
# Commit.between(repo, '29eda46b', 'master')
#
def between(repo, base, head)
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repo.gitaly_commit_client.between(base, head)
end
end
@@ -125,7 +126,7 @@ module Gitlab
# are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
def find_all(repo, options = {})
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
end
end
@@ -142,7 +143,7 @@ module Gitlab
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repo.gitaly_commit_client.list_commits_by_oid(oids)
end
end
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index ae6f554bc06..83a9fd5f81a 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -3,6 +3,8 @@
module Gitlab
module Git
class CommitStats
+ include Gitlab::Git::WrapsGitalyErrors
+
attr_reader :id, :additions, :deletions, :total
# Instantiate a CommitStats object
@@ -14,7 +16,7 @@ module Gitlab
@deletions = 0
@total = 0
- repo.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_stats(repo, commit)
end
end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index 6dc792c16b8..307f1b8cb66 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -2,6 +2,8 @@ module Gitlab
module Git
module Conflict
class Resolver
+ include Gitlab::Git::WrapsGitalyErrors
+
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
@@ -12,7 +14,7 @@ module Gitlab
end
def conflicts
- @conflicts ||= @target_repository.wrapped_gitaly_errors do
+ @conflicts ||= wrapped_gitaly_errors do
gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
end
rescue GRPC::FailedPrecondition => e
@@ -22,7 +24,7 @@ module Gitlab
end
def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
- source_repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
end
end
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index f0fab1e76a3..d7148165408 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -6,8 +6,8 @@ module Gitlab
@newrev = newrev
end
- def new_pointers(object_limit: nil, not_in: nil)
- @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
+ def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil)
+ @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout)
end
def all_pointers
diff --git a/lib/gitlab/git/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb
index e4743b4db0a..7f9520de5ce 100644
--- a/lib/gitlab/git/remote_mirror.rb
+++ b/lib/gitlab/git/remote_mirror.rb
@@ -1,13 +1,15 @@
module Gitlab
module Git
class RemoteMirror
+ include Gitlab::Git::WrapsGitalyErrors
+
def initialize(repository, ref_name)
@repository = repository
@ref_name = ref_name
end
def update(only_branches_matching: [])
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
@repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching)
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9df04372cc2..fcc92341c40 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -6,6 +6,7 @@ module Gitlab
module Git
class Repository
include Gitlab::Git::RepositoryMirroring
+ include Gitlab::Git::WrapsGitalyErrors
include Gitlab::EncodingHelper
include Gitlab::Utils::StrongMemoize
@@ -845,23 +846,9 @@ module Gitlab
end
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
- Gitlab::GitalyClient.migrate(method, status: status, &block)
- rescue GRPC::NotFound => e
- raise NoRepository.new(e)
- rescue GRPC::InvalidArgument => e
- raise ArgumentError.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
- end
-
- def wrapped_gitaly_errors(&block)
- yield block
- rescue GRPC::NotFound => e
- raise NoRepository.new(e)
- rescue GRPC::InvalidArgument => e
- raise ArgumentError.new(e)
- rescue GRPC::BadStatus => e
- raise CommandError.new(e)
+ wrapped_gitaly_errors do
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
+ end
end
def clean_stale_repository_files
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index e0867aeb5a7..b5b701699f0 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -2,6 +2,7 @@ module Gitlab
module Git
class Tree
include Gitlab::EncodingHelper
+ extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :id, :root_id, :name, :path, :flat_path, :type,
:mode, :commit_id, :submodule_url
@@ -15,7 +16,7 @@ module Gitlab
def where(repository, sha, path = nil, recursive = false)
path = nil if path == '' || path == '/'
- repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 7fe56979d5c..02c643d0da0 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -1,6 +1,8 @@
module Gitlab
module Git
class Wiki
+ include Gitlab::Git::WrapsGitalyErrors
+
DuplicatePageError = Class.new(StandardError)
OperationError = Class.new(StandardError)
@@ -65,37 +67,37 @@ module Gitlab
end
def write_page(name, format, content, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_write_page(name, format, content, commit_details)
end
end
def delete_page(page_path, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_delete_page(page_path, commit_details)
end
end
def update_page(page_path, title, format, content, commit_details)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_update_page(page_path, title, format, content, commit_details)
end
end
def pages(limit: 0)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_get_all_pages(limit: limit)
end
end
def page(title:, version: nil, dir: nil)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_find_page(title: title, version: version, dir: dir)
end
end
def file(name, version)
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_find_file(name, version)
end
end
@@ -105,7 +107,7 @@ module Gitlab
# :per_page - The number of items per page.
# :limit - Total number of items to return.
def page_versions(page_path, options = {})
- versions = @repository.wrapped_gitaly_errors do
+ versions = wrapped_gitaly_errors do
gitaly_wiki_client.page_versions(page_path, options)
end
@@ -127,7 +129,7 @@ module Gitlab
def page_formatted_data(title:, dir: nil, version: nil)
version = version&.id
- @repository.wrapped_gitaly_errors do
+ wrapped_gitaly_errors do
gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
end
end
diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb
new file mode 100644
index 00000000000..4b161f7e6ce
--- /dev/null
+++ b/lib/gitlab/git/wraps_gitaly_errors.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Git
+ module WrapsGitalyErrors
+ def wrapped_gitaly_errors(&block)
+ yield block
+ rescue GRPC::NotFound => e
+ raise Gitlab::Git::Repository::NoRepository.new(e)
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError.new(e)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Git::CommandError.new(e)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 827c04ae035..802fa65dd63 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -9,6 +9,7 @@ module Gitlab
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
+ TimeoutError = Class.new(StandardError)
ProjectMovedError = Class.new(NotFoundError)
ERROR_MESSAGES = {
@@ -26,11 +27,18 @@ module Gitlab
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
}.freeze
+ INTERNAL_TIMEOUT = 50.seconds.freeze
+ LOG_HEADER = <<~MESSAGE
+ Push operation timed out
+
+ Timing information for debugging purposes:
+ MESSAGE
+
DOWNLOAD_COMMANDS = %w{git-upload-pack git-upload-archive}.freeze
PUSH_COMMANDS = %w{git-receive-pack}.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
- attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes
+ attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@@ -44,6 +52,7 @@ module Gitlab
end
def check(cmd, changes)
+ @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER)
@changes = changes
check_protocol!
@@ -269,14 +278,19 @@ module Gitlab
end
def check_single_change_access(change, skip_lfs_integrity_check: false)
- Checks::ChangeAccess.new(
+ change_access = Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
- protocol: protocol
- ).exec
+ protocol: protocol,
+ logger: logger
+ )
+
+ change_access.exec
+ rescue Checks::TimedLogger::TimeoutError
+ raise TimeoutError, logger.full_message
end
def deploy_key
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index e731e654f3c..cf2329e489d 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -11,8 +11,8 @@ module Gitlab
@changes = deserialize_changes(changes)
end
- def identify(revision)
- super(identifier, project, revision)
+ def identify
+ super(identifier)
end
def changes_refs
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 1840bf45154..086ce31e678 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -72,7 +72,7 @@ module Gitlab
GitalyClient::BlobsStitcher.new(response)
end
- def get_new_lfs_pointers(revision, limit, not_in)
+ def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil)
request = Gitaly::GetNewLFSPointersRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
@@ -85,7 +85,20 @@ module Gitlab
request.not_in_refs += not_in
end
- response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout)
+ timeout =
+ if dynamic_timeout
+ [dynamic_timeout, GitalyClient.medium_timeout].min
+ else
+ GitalyClient.medium_timeout
+ end
+
+ response = GitalyClient.call(
+ @gitaly_repo.storage_name,
+ :blob_service,
+ :get_new_lfs_pointers,
+ request,
+ timeout: timeout
+ )
map_lfs_pointers(response)
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 2956ed4b911..d7b36946b65 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -349,7 +349,7 @@ module Gitlab
f.write(message.data)
end
end
- # If the file is empty means that we recieved an empty stream, we delete the file
+ # If the file is empty means that we received an empty stream, we delete the file
FileUtils.rm(save_path) if File.zero?(save_path)
end
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
index 0014ce2689b..41004408dec 100644
--- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -6,7 +6,7 @@ module Gitlab
def call(severity, datetime, _, data)
time = data.delete :time
- data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params)
+ data[:params] = process_params(data)
attributes = {
time: datetime.utc.iso8601(3),
@@ -20,6 +20,14 @@ module Gitlab
private
+ def process_params(data)
+ return [] unless data.has_key?(:params)
+
+ data[:params]
+ .each_pair
+ .map { |k, v| { key: k, value: utf8_encode_values(v) } }
+ end
+
def utf8_encode_values(data)
case data
when Hash
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 83095acc528..a4e60bbd828 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -4,22 +4,25 @@ module Gitlab
class Highlight
TIMEOUT_BACKGROUND = 30.seconds
TIMEOUT_FOREGROUND = 3.seconds
+ MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte
- def self.highlight(blob_name, blob_content, repository: nil, plain: false)
- new(blob_name, blob_content, repository: repository)
+ def self.highlight(blob_name, blob_content, language: nil, plain: false)
+ new(blob_name, blob_content, language: language)
.highlight(blob_content, continue: false, plain: plain)
end
attr_reader :blob_name
- def initialize(blob_name, blob_content, repository: nil)
+ def initialize(blob_name, blob_content, language: nil)
@formatter = Rouge::Formatters::HTMLGitlab
- @repository = repository
+ @language = language
@blob_name = blob_name
@blob_content = blob_content
end
def highlight(text, continue: true, plain: false)
+ plain ||= text.length > MAXIMUM_TEXT_HIGHLIGHT_SIZE
+
highlighted_text = highlight_text(text, continue: continue, plain: plain)
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
highlighted_text
@@ -36,11 +39,9 @@ module Gitlab
private
def custom_language
- language_name = @repository && @repository.gitattribute(@blob_name, 'gitlab-language')
-
- return nil unless language_name
+ return nil unless @language
- Rouge::Lexer.find_fancy(language_name)
+ Rouge::Lexer.find_fancy(@language)
end
def highlight_text(text, continue: true, plain: false)
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 28a9fe29465..d5f94ad04f1 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
# Detect user based on identifier like
-# key-13 or user-36 or last commit
+# key-13 or user-36
module Gitlab
module Identifier
- def identify(identifier, project = nil, newrev = nil)
- if identifier.blank?
- identify_using_commit(project, newrev)
- elsif identifier =~ /\Auser-\d+\Z/
+ def identify(identifier)
+ if identifier =~ /\Auser-\d+\Z/
# git push over http
identify_using_user(identifier)
elsif identifier =~ /\Akey-\d+\Z/
@@ -16,19 +14,6 @@ module Gitlab
end
end
- # Tries to identify a user based on a commit SHA.
- def identify_using_commit(project, ref)
- return if project.nil? && ref.nil?
-
- commit = project.commit(ref)
-
- return if !commit || !commit.author_email
-
- identify_with_cache(:email, commit.author_email) do
- commit.author
- end
- end
-
# Tries to identify a user based on a user identifier (e.g. "user-123").
# rubocop: disable CodeReuse/ActiveRecord
def identify_using_user(identifier)
diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb
index 97dc1a987c4..9215067d973 100644
--- a/lib/gitlab/import/merge_request_helpers.rb
+++ b/lib/gitlab/import/merge_request_helpers.rb
@@ -22,7 +22,7 @@ module Gitlab
# additional work that is strictly necessary.
merge_request_id = insert_and_return_id(attributes, project.merge_requests)
- merge_request = project.merge_requests.find(merge_request_id)
+ merge_request = project.merge_requests.reload.find(merge_request_id)
# We use .insert_and_return_id which effectively disables all callbacks.
# Trigger iid logic here to make sure we track internal id values consistently.
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3d693d23c99..99581eb0416 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -154,7 +154,7 @@ module Gitlab
Project.transaction do
process_sub_relation(relation, relation_item)
- # For every subrelation that hangs from Project, save the associated records alltogether
+ # For every subrelation that hangs from Project, save the associated records altogether
# This effectively batches all records per subrelation item, only keeping those in memory
# We have to keep in mind that more batch granularity << Memory, but >> Slowness
if save
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 4a1bdf34c3e..1cd4f9e17b7 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -2,6 +2,7 @@ module Gitlab
module Kubernetes
module Helm
HELM_VERSION = '2.7.2'.freeze
+ KUBECTL_VERSION = '1.11.0'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
SERVICE_ACCOUNT = 'tiller'.freeze
CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 6752f2cff43..008cba9d33c 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -11,12 +11,6 @@ module Gitlab
def generate_script
<<~HEREDOC
set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
HEREDOC
end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index 95192b11c0d..e9c621d96f0 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -25,7 +25,7 @@ module Gitlab
def container_specification
{
name: 'helm',
- image: 'alpine:3.6',
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/#{Gitlab::Kubernetes::Helm::HELM_VERSION}-kube-#{Gitlab::Kubernetes::Helm::KUBECTL_VERSION}",
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
diff --git a/lib/gitlab/kubernetes/role_binding.rb b/lib/gitlab/kubernetes/role_binding.rb
index 4f3ee040bf2..cb0cb42d007 100644
--- a/lib/gitlab/kubernetes/role_binding.rb
+++ b/lib/gitlab/kubernetes/role_binding.rb
@@ -3,9 +3,8 @@
module Gitlab
module Kubernetes
class RoleBinding
- attr_reader :role_name, :namespace, :service_account_name
-
- def initialize(role_name:, namespace:, service_account_name:)
+ def initialize(name:, role_name:, namespace:, service_account_name:)
+ @name = name
@role_name = role_name
@namespace = namespace
@service_account_name = service_account_name
@@ -21,14 +20,16 @@ module Gitlab
private
+ attr_reader :name, :role_name, :namespace, :service_account_name
+
def metadata
- { name: "gitlab-#{namespace}", namespace: namespace }
+ { name: name, namespace: namespace }
end
def role_ref
{
apiGroup: 'rbac.authorization.k8s.io',
- kind: 'Role',
+ kind: 'ClusterRole',
name: role_name
}
end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 44025650de0..fa68dead80b 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -236,7 +236,7 @@ module Gitlab
def single_line_regexp(regex)
# Turns a multiline extended regexp into a single line one,
- # beacuse `rake routes` breaks on multiline regexes.
+ # because `rake routes` breaks on multiline regexes.
Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 3a202d915e3..04df881bf03 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -82,7 +82,7 @@ module Gitlab
ref: ref,
startline: startline,
data: data.join,
- project_id: project ? project.id : nil
+ project: project
)
end
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb
index 82213098672..a64cb47e77e 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/proxy_http_connection_adapter.rb
@@ -4,7 +4,7 @@
# of the global setting allow_local_requests_from_hooks_and_services this adapter
# will allow/block connection to internal IPs and/or urls.
#
-# This functionality can be overriden by providing the setting the option
+# This functionality can be overridden by providing the setting the option
# allow_local_requests = true in the request. For example:
# Gitlab::HTTP.get('http://www.gitlab.com', allow_local_requests: true)
#
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 5ce3eda2ccb..458737f31eb 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -4,8 +4,10 @@ module Gitlab
class SearchResults
class FoundBlob
include EncodingHelper
+ include Presentable
+ include BlobLanguageFromGitAttributes
- attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
+ attr_reader :id, :filename, :basename, :ref, :startline, :data, :project
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@@ -15,6 +17,11 @@ module Gitlab
@startline = opts.fetch(:startline, nil)
@data = encode_utf8(opts.fetch(:data, nil))
@per_page = opts.fetch(:per_page, 20)
+ @project = opts.fetch(:project, nil)
+ # Some caller does not have project object (e.g. elastic search),
+ # yet they can trigger many calls in one go,
+ # causing duplicated queries.
+ # Allow those to just pass project_id instead.
@project_id = opts.fetch(:project_id, nil)
end
@@ -22,8 +29,12 @@ module Gitlab
filename
end
- def no_highlighting?
- false
+ def project_id
+ @project_id || @project&.id
+ end
+
+ def present
+ super(presenter_class: BlobPresenter)
end
end
diff --git a/lib/gitlab/slash_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb
index 25f965e843d..6396b828dc7 100644
--- a/lib/gitlab/slash_commands/issue_new.rb
+++ b/lib/gitlab/slash_commands/issue_new.rb
@@ -3,7 +3,7 @@ module Gitlab
class IssueNew < IssueCommand
def self.match(text)
# we can not match \n with the dot by passing the m modifier as than
- # the title and description are not seperated
+ # the title and description are not separated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 7735b736689..86efe8ad114 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -32,6 +32,7 @@ module Gitlab
end
validate_localhost!(addrs_info) unless allow_localhost
+ validate_loopback!(addrs_info) unless allow_localhost
validate_local_network!(addrs_info) unless allow_local_network
validate_link_local!(addrs_info) unless allow_local_network
@@ -86,6 +87,12 @@ module Gitlab
raise BlockedUrlError, "Requests to localhost are not allowed"
end
+ def validate_loopback!(addrs_info)
+ return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
+
+ raise BlockedUrlError, "Requests to loopback addresses are not allowed"
+ end
+
def validate_local_network!(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb
index bd0d24e4369..874599688bb 100644
--- a/lib/gitlab/user_extractor.rb
+++ b/lib/gitlab/user_extractor.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# This class extracts all users found in a piece of text by the username or the
-# email adress
+# email address
module Gitlab
class UserExtractor
@@ -14,13 +14,11 @@ module Gitlab
@text = text
end
- # rubocop: disable CodeReuse/ActiveRecord
def users
return User.none unless @text.present?
@users ||= User.from_union(union_relations)
end
- # rubocop: enable CodeReuse/ActiveRecord
def usernames
matches[:usernames]
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
index e724e58e9ca..56f056fd869 100644
--- a/lib/google_api/auth.rb
+++ b/lib/google_api/auth.rb
@@ -16,7 +16,7 @@ module GoogleApi
client.auth_code.authorize_url(
redirect_uri: redirect_uri,
scope: scope,
- state: state # This is used for arbitary redirection
+ state: state # This is used for arbitrary redirection
)
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
index 81829668de8..eec024f9bbb 100644
--- a/lib/tasks/tokens.rake
+++ b/lib/tasks/tokens.rake
@@ -1,4 +1,7 @@
require_relative '../../app/models/concerns/token_authenticatable.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/base.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/insecure.rb'
+require_relative '../../app/models/concerns/token_authenticatable_strategies/digest.rb'
namespace :tokens do
desc "Reset all GitLab incoming email tokens"
@@ -26,13 +29,6 @@ class TmpUser < ActiveRecord::Base
self.table_name = 'users'
- def reset_incoming_email_token!
- write_new_token(:incoming_email_token)
- save!(validate: false)
- end
-
- def reset_feed_token!
- write_new_token(:feed_token)
- save!(validate: false)
- end
+ add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
+ add_authentication_token_field :feed_token
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6c5538f029b..3a100a099aa 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -648,6 +648,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
+msgid "Are you sure you want to stop this environment?"
+msgstr ""
+
msgid "Are you sure?"
msgstr ""
@@ -1983,6 +1986,9 @@ msgstr ""
msgid "Copy token to clipboard"
msgstr ""
+msgid "Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}"
+msgstr ""
+
msgid "Create"
msgstr ""
@@ -2324,6 +2330,12 @@ msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
+msgid "Deployed to"
+msgstr ""
+
+msgid "Deploying to"
+msgstr ""
+
msgid "Deprioritize label"
msgstr ""
@@ -2750,6 +2762,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to deploy to"
+msgstr ""
+
msgid "Failed to load emoji list."
msgstr ""
@@ -3002,9 +3017,15 @@ msgstr ""
msgid "Group Runners"
msgstr ""
+msgid "Group URL"
+msgstr ""
+
msgid "Group avatar"
msgstr ""
+msgid "Group description"
+msgstr ""
+
msgid "Group description (optional)"
msgstr ""
@@ -4030,9 +4051,6 @@ msgstr ""
msgid "No"
msgstr ""
-msgid "No Assignee"
-msgstr ""
-
msgid "No Label"
msgstr ""
@@ -4482,15 +4500,24 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
+msgid "Pipeline|Commit"
+msgstr ""
+
msgid "Pipeline|Create for"
msgstr ""
msgid "Pipeline|Create pipeline"
msgstr ""
+msgid "Pipeline|Duration"
+msgstr ""
+
msgid "Pipeline|Existing branch name or tag"
msgstr ""
+msgid "Pipeline|Pipeline"
+msgstr ""
+
msgid "Pipeline|Run Pipeline"
msgstr ""
@@ -4500,6 +4527,12 @@ msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
+msgid "Pipeline|Stages"
+msgstr ""
+
+msgid "Pipeline|Status"
+msgstr ""
+
msgid "Pipeline|Stop pipeline"
msgstr ""
@@ -4536,12 +4569,18 @@ msgstr ""
msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please choose a group URL with no special characters."
+msgstr ""
+
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
msgstr ""
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr ""
+msgid "Please fill in a descriptive name for your group."
+msgstr ""
+
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr ""
@@ -4893,6 +4932,9 @@ msgstr ""
msgid "Projects shared with %{group_name}"
msgstr ""
+msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -5610,6 +5652,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching the environments for this merge request. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -5787,6 +5832,12 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start and due date"
+msgstr ""
+
+msgid "Start date"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -6774,6 +6825,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
+msgid "View the documentation"
+msgstr ""
+
msgid "Visibility and access controls"
msgstr ""
@@ -6825,6 +6879,9 @@ msgstr ""
msgid "Who can see this group?"
msgstr ""
+msgid "Who will be able to see this group?"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -6999,9 +7056,6 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
-msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
-msgstr ""
-
msgid "You can easily contribute to them by requesting to join these groups."
msgstr ""
@@ -7023,6 +7077,9 @@ msgstr ""
msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas."
msgstr ""
+msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
+msgstr ""
+
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
diff --git a/package.json b/package.json
index 086617dc265..2d6479fea3f 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,8 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/preset-env": "^7.1.0",
- "@gitlab-org/gitlab-svgs": "^1.33.0",
- "@gitlab-org/gitlab-ui": "^1.8.0",
+ "@gitlab-org/gitlab-ui": "^1.10.0",
+ "@gitlab/svgs": "^1.35.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-loader": "^8.0.4",
diff --git a/qa/qa.rb b/qa/qa.rb
index 968687dab67..f00331dfe93 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -39,7 +39,6 @@ module QA
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
- autoload :Product, 'qa/factory/product'
module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox'
@@ -53,7 +52,7 @@ module QA
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :DeployToken, 'qa/factory/resource/deploy_token'
autoload :Branch, 'qa/factory/resource/branch'
- autoload :SecretVariable, 'qa/factory/resource/secret_variable'
+ autoload :CiVariable, 'qa/factory/resource/ci_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
@@ -183,7 +182,7 @@ module QA
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens'
autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches'
- autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
+ autoload :CiVariables, 'qa/page/project/settings/ci_variables'
autoload :Runners, 'qa/page/project/settings/runners'
autoload :MergeRequest, 'qa/page/project/settings/merge_request'
autoload :Members, 'qa/page/project/settings/members'
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
index cfce096ab39..42077f60611 100644
--- a/qa/qa/factory/README.md
+++ b/qa/qa/factory/README.md
@@ -88,44 +88,6 @@ end
The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations.
-### Define attributes
-
-After the resource is fabricated, we would like to access the attributes on
-the resource. We define the attributes with `attribute` method. Suppose
-we want to access the name on the resource, we could change `attr_accessor`
-to `attribute`:
-
-```ruby
-module QA
- module Factory
- module Resource
- class Shirt < Factory::Base
- attribute :name
-
- # ... same as before
- end
- end
- end
-end
-```
-
-The difference between `attr_accessor` and `attribute` is that by using
-`attribute` it can also be accessed from the product:
-
-```ruby
-shirt =
- QA::Factory::Resource::Shirt.fabricate! do |resource|
- resource.name = "GitLab QA"
- end
-
-shirt.name # => "GitLab QA"
-```
-
-In the above example, if we use `attr_accessor :name` then `shirt.name` won't
-be available. On the other hand, using `attribute :name` will allow you to use
-`shirt.name`, so most of the time you'll want to use `attribute` instead of
-`attr_accessor` unless we clearly don't need it for the product.
-
#### Resource attributes
A resource may need another resource to exist first. For instance, a project
@@ -145,7 +107,7 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attribute :name
+ attr_accessor :name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
@@ -206,7 +168,7 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attribute :name
+ attr_accessor :name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
@@ -287,7 +249,7 @@ module QA
shirt_new.create_shirt!
end
- brand # Eagerly construct the data
+ populate(:brand) # Eagerly construct the data
end
end
end
@@ -295,9 +257,12 @@ module QA
end
```
-This will make sure we construct the data right after we created the shirt.
-The drawback for this will become we're forced to construct the data even
-if we don't really need to use it.
+The `populate` method will iterate through its arguments and call each
+attribute respectively. Here `populate(:brand)` has the same effect as
+just `brand`. Using the populate method makes the intention clearer.
+
+With this, it will make sure we construct the data right after we create the
+shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data.
Alternatively, we could just make sure we're on the right page before
constructing the brand data:
@@ -307,7 +272,7 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
- attribute :name
+ attr_accessor :name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
@@ -385,7 +350,7 @@ end
**Notes on attributes precedence:**
-- attributes from the factory have the highest precedence
+- factory instance variables have the highest precedence
- attributes from the API response take precedence over attributes from the
block (usually from Browser UI)
- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index e82e16f9415..75438b77bf3 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -22,12 +22,16 @@ module QA
visit(web_url)
end
+ def populate(*attributes)
+ attributes.each(&method(:public_send))
+ end
+
private
def populate_attribute(name, block)
value = attribute_value(name, block)
- raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
+ raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value
value
end
@@ -84,7 +88,7 @@ module QA
resource_web_url = yield
factory.web_url = resource_web_url
- Factory::Product.new(factory)
+ factory
end
private_class_method :do_fabricate!
@@ -96,7 +100,7 @@ module QA
msg = [prefix]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
- msg << "via #{method} with args #{args}"
+ msg << "via #{method}"
yield.tap do
msg << "in #{Time.now - start} seconds"
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
deleted file mode 100644
index 34df0bda8e5..00000000000
--- a/qa/qa/factory/product.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'capybara/dsl'
-
-module QA
- module Factory
- class Product
- include Capybara::DSL
-
- attr_reader :factory
-
- def initialize(factory)
- @factory = factory
-
- define_attributes
- end
-
- def visit!
- visit(web_url)
- end
-
- def populate(*attributes)
- attributes.each(&method(:public_send))
- end
-
- private
-
- def define_attributes
- factory.class.attributes_names.each do |name|
- define_singleton_method(name) do
- factory.public_send(name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
index a9dfbc0a783..272b7fc5818 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/factory/repository/project_push.rb
@@ -9,8 +9,6 @@ module QA
end
end
- attribute :output
-
def initialize
@file_name = 'file.txt'
@file_content = '# This is test project'
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 703c78daa99..ffa755b9e88 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -45,7 +45,7 @@ module QA
repository.use_ssh_key(ssh_key)
else
repository.uri = repository_http_uri
- repository.use_default_credentials
+ repository.use_default_credentials unless user
end
username = 'GitLab QA'
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/ci_variable.rb
index 24ba3408810..a0aefc61f9f 100644
--- a/qa/qa/factory/resource/secret_variable.rb
+++ b/qa/qa/factory/resource/ci_variable.rb
@@ -1,13 +1,13 @@
module QA
module Factory
module Resource
- class SecretVariable < Factory::Base
+ class CiVariable < Factory::Base
attr_accessor :key, :value
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
- resource.name = 'project-with-secret-variables'
- resource.description = 'project for adding secret variable test'
+ resource.name = 'project-with-ci-variables'
+ resource.description = 'project for adding CI variable test'
end
end
@@ -17,7 +17,7 @@ module QA
Page::Project::Menu.perform(&:click_ci_cd_settings)
Page::Project::Settings::CICD.perform do |setting|
- setting.expand_secret_variables do |page|
+ setting.expand_ci_variables do |page|
page.fill_variable(key, value)
page.save_variables
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
index 0fac4377040..b1e874af893 100644
--- a/qa/qa/factory/resource/fork.rb
+++ b/qa/qa/factory/resource/fork.rb
@@ -50,8 +50,7 @@ module QA
end
def fabricate!
- push
- user
+ populate(:push, :user)
visit_project_with_retry
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index 92b8bdf4a21..4b7d2287f98 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -12,8 +12,6 @@ module QA
:milestone,
:labels
- attribute :source_branch
-
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-merge-request'
@@ -52,8 +50,8 @@ module QA
end
def fabricate!
- target
- source
+ populate(:target, :source)
+
project.visit!
Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page|
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
index fbe062539b9..1311bf625a6 100644
--- a/qa/qa/factory/resource/merge_request_from_fork.rb
+++ b/qa/qa/factory/resource/merge_request_from_fork.rb
@@ -18,8 +18,10 @@ module QA
end
def fabricate!
- push
+ populate(:push)
+
fork.visit!
+
Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform(&:create_merge_request)
end
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
index f62092ae122..ce20641e6cc 100644
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ b/qa/qa/factory/resource/project_imported_from_github.rb
@@ -4,14 +4,13 @@ module QA
module Factory
module Resource
class ProjectImportedFromGithub < Resource::Project
+ attr_accessor :name
attr_writer :personal_access_token, :github_repository_path
attribute :group do
Factory::Resource::Group.fabricate!
end
- attribute :name
-
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
index cfda58dc103..383f534c12c 100644
--- a/qa/qa/factory/resource/project_milestone.rb
+++ b/qa/qa/factory/resource/project_milestone.rb
@@ -2,14 +2,13 @@ module QA
module Factory
module Resource
class ProjectMilestone < Factory::Base
+ attr_reader :title
attr_accessor :description
attribute :project do
Factory::Resource::Project.fabricate!
end
- attribute :title
-
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
@description = 'A milestone'
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index 56bcda9e2f3..a125bac65dd 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -9,7 +9,6 @@ module QA
attr_reader :path
attribute :id
- attribute :path
def initialize
@path = Runtime::Namespace.sandbox_name
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
index a48a93fbe65..6f952eda36f 100644
--- a/qa/qa/factory/resource/ssh_key.rb
+++ b/qa/qa/factory/resource/ssh_key.rb
@@ -6,11 +6,9 @@ module QA
class SSHKey < Factory::Base
extend Forwardable
- def_delegators :key, :private_key, :public_key, :fingerprint
+ attr_accessor :title
- attribute :private_key
- attribute :title
- attribute :fingerprint
+ def_delegators :key, :private_key, :public_key, :fingerprint
def key
@key ||= Runtime::Key::RSA.new
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
index 6e6f46f7a95..e361face1f0 100644
--- a/qa/qa/factory/resource/user.rb
+++ b/qa/qa/factory/resource/user.rb
@@ -5,6 +5,7 @@ module QA
module Resource
class User < Factory::Base
attr_reader :unique_id
+ attr_writer :username, :password
def initialize
@unique_id = SecureRandom.hex(8)
@@ -30,11 +31,6 @@ module QA
defined?(@username) && defined?(@password)
end
- attribute :name
- attribute :username
- attribute :email
- attribute :password
-
def fabricate!
# Don't try to log-out if we're not logged-in
if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 1062f0b2dbb..de18b9cefa6 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -7,35 +7,42 @@ module QA
class Show < Page::Base
include Page::Component::Issuable::Common
- view 'app/views/projects/issues/show.html.haml' do
- element :issue_details, '.issue-details' # rubocop:disable QA/ElementWithPattern
- element :title, '.title' # rubocop:disable QA/ElementWithPattern
- end
-
view 'app/views/shared/notes/_form.html.haml' do
element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern
element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end
- view 'app/views/shared/notes/_comment_button.html.haml' do
- element :comment_button, '%strong Comment' # rubocop:disable QA/ElementWithPattern
+ view 'app/assets/javascripts/notes/components/comment_form.vue' do
+ element :comment_button
+ element :comment_input
end
- def issue_title
- find('.issue-details .title').text
+ view 'app/assets/javascripts/notes/components/discussion_filter.vue' do
+ element :discussion_filter
+ element :filter_options
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil)
- fill_in(with: text, name: 'note[note]')
+ fill_element :comment_input, text
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
- click_on 'Comment'
+ click_element :comment_button
+ end
+
+ def select_comments_only_filter
+ click_element :discussion_filter
+ all_elements(:filter_options).last.click
+ end
+
+ def select_all_activities_filter
+ click_element :discussion_filter
+ all_elements(:filter_options).first.click
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb
index 18c16ca6db7..939f912ea85 100644
--- a/qa/qa/page/project/operations/kubernetes/add.rb
+++ b/qa/qa/page/project/operations/kubernetes/add.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class Add < Page::Base
- view 'app/views/projects/clusters/new.html.haml' do
+ view 'app/views/clusters/clusters/new.html.haml' do
element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
index f8e026b4405..f3ab636ecc1 100644
--- a/qa/qa/page/project/operations/kubernetes/add_existing.rb
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class AddExisting < Page::Base
- view 'app/views/projects/clusters/user/_form.html.haml' do
+ view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
index 312b459ac89..67a74af1cd2 100644
--- a/qa/qa/page/project/operations/kubernetes/index.rb
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -4,7 +4,7 @@ module QA
module Operations
module Kubernetes
class Index < Page::Base
- view 'app/views/projects/clusters/_empty_state.html.haml' do
+ view 'app/views/clusters/clusters/_empty_state.html.haml' do
element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index cc5fc370a5a..12c2409a5a7 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -25,9 +25,9 @@ module QA # rubocop:disable Naming/FileName
end
end
- def expand_secret_variables(&block)
+ def expand_ci_variables(&block)
expand_section(:variables_settings) do
- Settings::SecretVariables.perform(&block)
+ Settings::CiVariables.perform(&block)
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 6a87ef472e4..e7a6e4bf628 100644
--- a/qa/qa/page/project/settings/secret_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -2,7 +2,7 @@ module QA
module Page
module Project
module Settings
- class SecretVariables < Page::Base
+ class CiVariables < Page::Base
include Common
view 'app/views/ci/variables/_variable_row.html.haml' do
@@ -12,7 +12,7 @@ module QA
end
view 'app/views/ci/variables/_index.html.haml' do
- element :save_variables, '.js-secret-variables-save-button' # rubocop:disable QA/ElementWithPattern
+ element :save_variables, '.js-ci-variables-save-button' # rubocop:disable QA/ElementWithPattern
element :reveal_values, '.js-secret-value-reveal-button' # rubocop:disable QA/ElementWithPattern
end
@@ -33,7 +33,7 @@ module QA
end
def save_variables
- find('.js-secret-variables-save-button').click
+ find('.js-ci-variables-save-button').click
end
def reveal_variables
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
new file mode 100644
index 00000000000..24877d937d2
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'filter issue comments activities' do
+ let(:issue_title) { 'issue title' }
+
+ it 'user filters comments and activites in an issue' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ Factory::Resource::Issue.fabricate! do |issue|
+ issue.title = issue_title
+ end
+
+ expect(page).to have_content(issue_title)
+
+ Page::Project::Issue::Show.perform do |show_page|
+ show_page.select_comments_only_filter
+ show_page.comment('/confidential')
+ show_page.comment('My own comment')
+
+ expect(show_page).not_to have_content("made the issue confidential")
+ expect(show_page).to have_content("My own comment")
+
+ show_page.select_all_activities_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).to have_content("My own comment")
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index 46e1005829d..724c48cd125 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -25,6 +25,7 @@ module QA
push.file_content = "Test with unicode characters ❤✓€❄"
end
+ Page::Project::Show.perform(&:wait_for_push)
merge_request.visit!
expect(page).to have_text('to be squashed')
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
new file mode 100644
index 00000000000..8e4210482a2
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Git push over HTTP', :ldap_no_tls do
+ it 'user using a personal access token pushes code to the repository' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token
+
+ user = Factory::Resource::User.new.tap do |user|
+ user.username = Runtime::User.username
+ user.password = access_token
+ end
+
+ push = Factory::Repository::ProjectPush.fabricate! do |push|
+ push.user = user
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
+ end
+
+ push.project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ expect(page).to have_content('README.md')
+ expect(page).to have_content('This is a test project')
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 292f24d9c0d..58b272adcf1 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -2,24 +2,24 @@
module QA
context 'Verify' do
- describe 'Secret variable support' do
- it 'user adds a secret variable' do
+ describe 'CI variable support' do
+ it 'user adds a CI variable' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Factory::Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
- resource.value = 'some secret variable'
+ resource.value = 'some CI variable'
end
Page::Project::Settings::CICD.perform do |settings|
- settings.expand_secret_variables do |page|
+ settings.expand_ci_variables do |page|
expect(page).to have_field(with: 'VARIABLE_KEY')
- expect(page).not_to have_field(with: 'some secret variable')
+ expect(page).not_to have_field(with: 'some CI variable')
page.reveal_variables
- expect(page).to have_field(with: 'some secret variable')
+ expect(page).to have_field(with: 'some CI variable')
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index caf014c89ea..604641e54b8 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -55,7 +55,7 @@ module QA
deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Factory::Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = deploy_key_name
resource.value = key.private_key
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 40cae0793dd..c2fce1e7df1 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -22,7 +22,7 @@ module QA
# Disable code_quality check in Auto DevOps pipeline as it takes
# too long and times out the test
- Factory::Resource::SecretVariable.fabricate! do |resource|
+ Factory::Resource::CiVariable.fabricate! do |resource|
resource.project = project
resource.key = 'CODE_QUALITY_DISABLED'
resource.value = '1'
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index d7b92052894..990eba76460 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/factory/base_spec.rb
@@ -4,8 +4,7 @@ describe QA::Factory::Base do
include Support::StubENV
let(:factory) { spy('factory') }
- let(:product) { spy('product') }
- let(:product_location) { 'http://product_location' }
+ let(:location) { 'http://location' }
shared_context 'fabrication context' do
subject do
@@ -17,9 +16,8 @@ describe QA::Factory::Base do
end
before do
- allow(subject).to receive(:current_url).and_return(product_location)
+ allow(subject).to receive(:current_url).and_return(location)
allow(subject).to receive(:new).and_return(factory)
- allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
end
end
@@ -28,7 +26,7 @@ describe QA::Factory::Base do
it 'yields factory before calling factory method' do
expect(factory).to receive(:something!).ordered
- expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location)
+ expect(factory).to receive(fabrication_method_used).ordered.and_return(location)
subject.public_send(fabrication_method_called, factory: factory) do |factory|
factory.something!
@@ -37,7 +35,7 @@ describe QA::Factory::Base do
it 'does not log the factory and build method when QA_DEBUG=false' do
stub_env('QA_DEBUG', 'false')
- expect(factory).to receive(fabrication_method_used).and_return(product_location)
+ expect(factory).to receive(fabrication_method_used).and_return(location)
expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
.not_to output.to_stdout
@@ -71,20 +69,20 @@ describe QA::Factory::Base do
it_behaves_like 'fabrication method', :fabricate_via_api!
- it 'instantiates the factory, calls factory method returns fabrication product' do
- expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
+ it 'instantiates the factory, calls factory method returns the resource' do
+ expect(factory).to receive(:fabricate_via_api!).and_return(location)
result = subject.fabricate_via_api!(factory: factory, parents: [])
- expect(result).to eq(product)
+ expect(result).to eq(factory)
end
it 'logs the factory and build method when QA_DEBUG=true' do
stub_env('QA_DEBUG', 'true')
- expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
+ expect(factory).to receive(:fabricate_via_api!).and_return(location)
- expect { subject.fabricate_via_api!(factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
+ expect { subject.fabricate_via_api!('something', factory: factory, parents: []) }
+ .to output(/==> Built a MyFactory via api in [\d\.\-e]+ seconds+/)
.to_stdout
end
end
@@ -100,17 +98,17 @@ describe QA::Factory::Base do
expect(factory).to have_received(:fabricate!).with('something')
end
- it 'returns fabrication product' do
+ it 'returns fabrication resource' do
result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
- expect(result).to eq(product)
+ expect(result).to eq(factory)
end
it 'logs the factory and build method when QA_DEBUG=true' do
stub_env('QA_DEBUG', 'true')
expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
- .to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/)
+ .to output(/==> Built a MyFactory via browser_ui in [\d\.\-e]+ seconds+/)
.to_stdout
end
end
@@ -140,44 +138,44 @@ describe QA::Factory::Base do
describe '.attribute' do
include_context 'simple factory'
- it 'appends new product attribute' do
+ it 'appends new attribute' do
expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
end
- context 'when the product attribute is populated via a block' do
- it 'returns a fabrication product and defines factory attributes as its methods' do
+ context 'when the attribute is populated via a block' do
+ it 'returns value from the block' do
result = subject.fabricate!(factory: factory)
- expect(result).to be_a(QA::Factory::Product)
+ expect(result).to be_a(described_class)
expect(result.test).to eq('block')
end
end
- context 'when the product attribute is populated via the api' do
+ context 'when the attribute is populated via the api' do
let(:api_resource) { { no_block: 'api' } }
before do
expect(factory).to receive(:api_resource).and_return(api_resource)
end
- it 'returns a fabrication product and defines factory attributes as its methods' do
+ it 'returns value from api' do
result = subject.fabricate!(factory: factory)
- expect(result).to be_a(QA::Factory::Product)
+ expect(result).to be_a(described_class)
expect(result.no_block).to eq('api')
end
- context 'when the attribute also has a block in the factory' do
+ context 'when the attribute also has a block' do
let(:api_resource) { { test: 'api_with_block' } }
before do
allow(QA::Runtime::Logger).to receive(:info)
end
- it 'returns the api value and emits an INFO log entry' do
+ it 'returns value from api and emits an INFO log entry' do
result = subject.fabricate!(factory: factory)
- expect(result).to be_a(QA::Factory::Product)
+ expect(result).to be_a(described_class)
expect(result.test).to eq('api_with_block')
expect(QA::Runtime::Logger)
.to have_received(:info).with(/api_with_block/)
@@ -185,15 +183,15 @@ describe QA::Factory::Base do
end
end
- context 'when the product attribute is populated via a factory attribute' do
+ context 'when the attribute is populated via direct assignment' do
before do
factory.test = 'value'
end
- it 'returns a fabrication product and defines factory attributes as its methods' do
+ it 'returns value from the assignment' do
result = subject.fabricate!(factory: factory)
- expect(result).to be_a(QA::Factory::Product)
+ expect(result).to be_a(described_class)
expect(result.test).to eq('value')
end
@@ -202,21 +200,21 @@ describe QA::Factory::Base do
allow(factory).to receive(:api_resource).and_return({ test: 'api' })
end
- it 'returns the factory attribute for the product' do
+ it 'returns value from the assignment' do
result = subject.fabricate!(factory: factory)
- expect(result).to be_a(QA::Factory::Product)
+ expect(result).to be_a(described_class)
expect(result.test).to eq('value')
end
end
end
- context 'when the product attribute has no value' do
+ context 'when the attribute has no value' do
it 'raises an error because no values could be found' do
result = subject.fabricate!(factory: factory)
expect { result.no_block }
- .to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.")
+ .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.")
end
end
end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
deleted file mode 100644
index 5b6eaa13e9c..00000000000
--- a/qa/spec/factory/product_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-describe QA::Factory::Product do
- let(:factory) do
- Class.new(QA::Factory::Base) do
- attribute :test do
- 'block'
- end
-
- attribute :no_block
- end.new
- end
-
- let(:product) { spy('product') }
- let(:product_location) { 'http://product_location' }
-
- subject { described_class.new(factory) }
-
- before do
- factory.web_url = product_location
- end
-
- describe '.visit!' do
- it 'makes it possible to visit fabrication product' do
- allow_any_instance_of(described_class)
- .to receive(:visit).and_return('visited some url')
-
- expect(subject.visit!).to eq 'visited some url'
- end
- end
-end
diff --git a/rubocop/cop/code_reuse/active_record.rb b/rubocop/cop/code_reuse/active_record.rb
index d25e8548fd0..2be8f7c11aa 100644
--- a/rubocop/cop/code_reuse/active_record.rb
+++ b/rubocop/cop/code_reuse/active_record.rb
@@ -49,7 +49,6 @@ module RuboCop
limit: true,
lock: false,
many?: false,
- none: false,
offset: true,
order: true,
pluck: true,
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
new file mode 100755
index 00000000000..1d77524d503
--- /dev/null
+++ b/scripts/build_assets_image
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Generate the image name based on the project this is being run in
+ASSETS_IMAGE_NAME=$(echo ${CI_PROJECT_NAME} |
+ awk '{
+ split($1, p, "-");
+ interim = sprintf("%s-assets-%s", p[1], p[2]);
+ sub(/-$/, "", interim);
+ print interim
+ }'
+)
+
+ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME}
+
+mkdir -p assets_container.build/public
+cp -r public/assets assets_container.build/public/
+cp Dockerfile.assets assets_container.build/
+docker build -t ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} -f assets_container.build/Dockerfile.assets assets_container.build/
+docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
+docker push ${ASSETS_IMAGE_PATH}
+
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 0e67eabfec1..25ba7ec6c8e 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -29,6 +29,7 @@ tasks = [
%w[bin/rake lint:all],
%w[bundle exec license_finder],
%w[yarn run eslint],
+ %w[yarn run prettier-all],
%w[bundle exec rubocop --parallel],
%w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged]
diff --git a/scripts/trigger-build b/scripts/trigger-build
index b76cd5dd6f0..dd0425b6472 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -32,32 +32,32 @@ module Trigger
private
- # Must be overriden
+ # Must be overridden
def downstream_project_path
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def ref
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def trigger_token
raise NotImplementedError
end
- # Must be overriden
+ # Must be overridden
def access_token
raise NotImplementedError
end
- # Can be overriden
+ # Can be overridden
def extra_variables
{}
end
- # Can be overriden
+ # Can be overridden
def version_param_value(version_file)
File.read(version_file).strip
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index be3fc832008..4e91068ab88 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -792,4 +792,30 @@ describe ApplicationController do
end
end
end
+
+ context 'control headers' do
+ controller(described_class) do
+ def index
+ render json: :ok
+ end
+ end
+
+ context 'user not logged in' do
+ it 'sets the default headers' do
+ get :index
+
+ expect(response.headers['Cache-Control']).to be_nil
+ end
+ end
+
+ context 'user logged in' do
+ it 'sets the default headers' do
+ sign_in(user)
+
+ get :index
+
+ expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
+ end
+ end
+ end
end
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 56047c0c8d2..278b980b6d8 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do
end
describe "#index" do
+ render_views
+
it 'returns group and project milestones to which the user belongs' do
get :index, format: :json
@@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do
expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id])
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
end
+
+ it 'should contain group and project milestones to which the user belongs to' do
+ get :index
+
+ expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>")
+ expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
+ end
end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index d1d08391164..99429c93b82 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -32,9 +32,10 @@ describe Groups::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, group: group)
+ visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
list_boards
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 465f3499703..42723bb3820 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -63,7 +63,7 @@ describe Groups::MilestonesController do
let(:group_milestone) { create(:milestone, group: group) }
context 'when there is a title parameter' do
- it 'searchs for a legacy group milestone' do
+ it 'searches for a legacy group milestone' do
expect(GlobalMilestone).to receive(:build)
expect(Milestone).not_to receive(:find_by_iid)
@@ -72,7 +72,7 @@ describe Groups::MilestonesController do
end
context 'when there is not a title parameter' do
- it 'searchs for a group milestone' do
+ it 'searches for a group milestone' do
expect(GlobalMilestone).not_to receive(:build)
expect(Milestone).to receive(:find_by_iid)
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a099cdafa58..4de61b65f71 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -529,7 +529,7 @@ describe GroupsController do
sign_in(user)
end
- context 'when transfering to a subgroup goes right' do
+ context 'when transferring to a subgroup goes right' do
let(:new_parent_group) { create(:group, :public) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 363ed410bc0..ea26bc83353 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -4,7 +4,7 @@ describe Profiles::KeysController do
let(:user) { create(:user) }
describe "#get_keys" do
- describe "non existant user" do
+ describe "non existent user" do
it "does not generally work" do
get :get_keys, username: 'not-existent'
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 28f7e4634a5..64b589a6d83 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/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 667eaa5e34f..8d503f6ad32 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -38,9 +38,10 @@ describe Projects::BoardsController do
end
it 'renders template if visited board is not found' do
- visited = double
+ temporary_board = create(:board, project: project)
+ visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user)
+ temporary_board.delete
- allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
list_boards
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index 9e17e392d3d..8106453a775 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Clusters::ApplicationsController do
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 9201332c5c8..04aece26590 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
+ include KubernetesHelpers
set(:project) { create(:project) }
@@ -218,9 +221,9 @@ describe Projects::ClustersController do
describe 'security' do
before do
allow_any_instance_of(described_class)
- .to receive(:token_in_session).and_return('token')
+ .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)
+ .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(
@@ -307,6 +310,11 @@ describe Projects::ClustersController do
end
describe 'security' do
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
+ end
+
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
@@ -318,14 +326,15 @@ describe Projects::ClustersController do
end
end
- describe 'GET status' do
+ describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
def go
- get :status, namespace_id: project.namespace,
- project_id: project,
- id: cluster,
- format: :json
+ get :cluster_status,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: cluster,
+ format: :json
end
describe 'functionality' do
@@ -359,9 +368,10 @@ describe Projects::ClustersController do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
def go
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
+ get :show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
describe 'functionality' do
@@ -401,13 +411,18 @@ describe Projects::ClustersController do
end
def go(format: :html)
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
+ put :update, params.merge(namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
id: cluster,
format: format
)
end
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ 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
@@ -530,9 +545,10 @@ describe Projects::ClustersController do
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
def go
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
+ delete :destroy,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
end
describe 'functionality' do
@@ -591,4 +607,10 @@ describe Projects::ClustersController do
it { expect { go }.to be_denied_for(:external) }
end
end
+
+ context 'no project_id param' do
+ it 'does not respond to any action without project_id param' do
+ expect { get :index }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 9e149bc4c3c..e34fdee62d6 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -356,6 +356,7 @@ describe Projects::CommitController do
expect(response).to be_ok
expect(JSON.parse(response.body)['pipelines']).not_to be_empty
expect(JSON.parse(response.body)['count']['all']).to eq 1
+ expect(response).to include_pagination_headers
end
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index b86029a4baf..bc17331f531 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -216,7 +216,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(200)
end
- it 'loads the terminals for the enviroment' do
+ it 'loads the terminals for the environment' do
expect_any_instance_of(Environment).to receive(:terminals)
get :terminal, environment_params
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 2023d4b0bd0..8eb01145ed5 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -152,11 +152,33 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
- expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
+ expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z})
expect(json_response['new_issue_path']).to include('/issues/new')
end
end
+ context 'when job is running' do
+ context 'job is cancelable' do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline) }
+
+ it 'cancel_path is present with correct redirect' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path']))
+ end
+ end
+
+ context 'with web terminal' do
+ let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
+
+ it 'exposes the terminal path' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/job_details')
+ expect(json_response['terminal_path']).to match(%r{/terminal})
+ end
+ end
+ end
+
context 'when job has artifacts' do
context 'with not expiry date' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
@@ -185,16 +207,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job has terminal' do
- let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
-
- it 'exposes the terminal path' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('job/job_details')
- expect(json_response['terminal_path']).to match(%r{/terminal})
- end
- end
-
context 'when job passed with no trace' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index dcfd6c05200..dafff4ee405 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -563,6 +563,7 @@ describe Projects::MergeRequestsController do
it 'responds with serialized pipelines' do
expect(json_response['pipelines']).not_to be_empty
expect(json_response['count']['all']).to eq 1
+ expect(response).to include_pagination_headers
end
end
@@ -749,13 +750,15 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do
- let!(:forked) { fork_project(project, user, repository: true) }
- let!(:environment) { create(:environment, project: forked) }
- let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
- let(:admin) { create(:admin) }
+ let(:forked) { fork_project(project, user, repository: true) }
+ let(:sha) { forked.commit.sha }
+ let(:environment) { create(:environment, project: forked) }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) }
let(:merge_request) do
- create(:merge_request, source_project: forked, target_project: project)
+ create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline)
end
it 'links to the environment on that project' do
@@ -764,6 +767,35 @@ describe Projects::MergeRequestsController do
expect(json_response.first['url']).to match /#{forked.full_path}/
end
+ context "when environment_target is 'merge_commit'" do
+ it 'returns nothing' do
+ get_ci_environments_status(environment_target: 'merge_commit')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+
+ context 'when is merged' do
+ let(:source_environment) { create(:environment, project: project) }
+ let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") }
+ let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) }
+ let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) }
+ let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) }
+
+ before do
+ merge_request.update!(merge_commit_sha: merge_commit_sha)
+ merge_request.mark_as_merged!
+ end
+
+ it 'returns the environment on the source project' do
+ get_ci_environments_status(environment_target: 'merge_commit')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.first['url']).to match /#{project.full_path}/
+ end
+ end
+ end
+
# we're trying to reduce the overall number of queries for this method.
# set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287
it 'keeps queries in check' do
@@ -772,11 +804,15 @@ describe Projects::MergeRequestsController do
expect(control_count).to be <= 137
end
- def get_ci_environments_status
- get :ci_environments_status,
+ def get_ci_environments_status(extra_params = {})
+ params = {
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
- id: merge_request.iid, format: 'json'
+ id: merge_request.iid,
+ format: 'json'
+ }
+
+ get :ci_environments_status, params.merge(extra_params)
end
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index bcf289f36a9..6420b70a54f 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -5,6 +5,17 @@ shared_examples 'content not cached without revalidation' do
end
end
+shared_examples 'content not cached without revalidation and no-store' do
+ it 'ensures content will not be cached without revalidation' do
+ # Fixed in newer versions of ActivePack, it will only output a single `private`.
+ if Gitlab.rails5?
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store')
+ else
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store')
+ end
+ end
+end
+
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
@@ -177,7 +188,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
@@ -239,7 +250,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -292,7 +303,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
@@ -344,7 +355,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -388,7 +399,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
@@ -445,7 +456,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
@@ -498,7 +509,7 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(200)
end
- it_behaves_like 'content not cached without revalidation' do
+ it_behaves_like 'content not cached without revalidation and no-store' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index bbeba8ce8b9..c9f5d0a813e 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -2,13 +2,28 @@ FactoryBot.define do
factory :cluster, class: Clusters::Cluster do
user
name 'test-cluster'
+ cluster_type :project_type
+
+ trait :instance do
+ cluster_type { Clusters::Cluster.cluster_types[:instance_type] }
+ end
trait :project do
+ cluster_type { Clusters::Cluster.cluster_types[:project_type] }
+
before(:create) do |cluster, evaluator|
cluster.projects << create(:project, :repository)
end
end
+ trait :group do
+ cluster_type { Clusters::Cluster.cluster_types[:group_type] }
+
+ before(:create) do |cluster, evalutor|
+ cluster.groups << create(:group)
+ end
+ end
+
trait :provided_by_user do
provider_type :user
platform_type :kubernetes
diff --git a/spec/factories/clusters/kubernetes_namespaces.rb b/spec/factories/clusters/kubernetes_namespaces.rb
index 6fdada75a3d..3f10f0ecc74 100644
--- a/spec/factories/clusters/kubernetes_namespaces.rb
+++ b/spec/factories/clusters/kubernetes_namespaces.rb
@@ -2,8 +2,18 @@
FactoryBot.define do
factory :cluster_kubernetes_namespace, class: Clusters::KubernetesNamespace do
- cluster
- project
- cluster_project
+ association :cluster, :project, :provided_by_gcp
+ namespace { |n| "environment#{n}" }
+
+ after(:build) do |kubernetes_namespace|
+ cluster_project = kubernetes_namespace.cluster.cluster_project
+
+ kubernetes_namespace.project = cluster_project.project
+ kubernetes_namespace.cluster_project = cluster_project
+ end
+
+ trait :with_token do
+ service_account_token { Faker::Lorem.characters(10) }
+ end
end
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 96dfde2e08c..735ca60f7da 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -53,13 +53,33 @@ describe 'Admin Groups' do
expect_selected_visibility(internal)
end
- it 'when entered in group path, it auto filled the group name', :js do
+ it 'when entered in group name, it auto filled the group path', :js do
visit admin_groups_path
click_link "New group"
- group_path = 'gitlab'
+ group_name = 'gitlab'
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq group_name
+ end
+
+ it 'auto populates the group path with the group name', :js do
+ visit admin_groups_path
+ click_link "New group"
+ group_name = 'my gitlab project'
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq 'my-gitlab-project'
+ end
+
+ it 'when entering in group path, group name does not change anymore', :js do
+ visit admin_groups_path
+ click_link "New group"
+ group_path = 'my-gitlab-project'
+ group_name = 'My modified gitlab project'
fill_in 'group_path', with: group_path
- name_field = find('input#group_name')
- expect(name_field.value).to eq group_path
+ fill_in 'group_name', with: group_name
+ path_field = find('input#group_path')
+ expect(path_field.value).to eq 'my-gitlab-project'
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 615223a2a88..2cdd3f55b50 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do
it 'filters by unassigned' do
set_filter('assignee')
- click_filter_link('No Assignee')
+ click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 8989b2051bb..5c6c1c4fd15 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -114,33 +114,6 @@ describe 'Commits' do
expect(page).to have_content 'canceled'
end
end
-
- describe '.gitlab-ci.yml not found warning' do
- context 'ci builds enabled' do
- it "does not show warning" do
- visit pipeline_path(pipeline)
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
-
- it 'shows warning' do
- stub_ci_pipeline_yaml_file(nil)
- visit pipeline_path(pipeline)
- expect(page).to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
-
- context 'ci builds disabled' do
- before do
- stub_ci_builds_disabled
- stub_ci_pipeline_yaml_file(nil)
- visit pipeline_path(pipeline)
- end
-
- it 'does not show warning' do
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
- end
end
context "when logged as reporter" do
@@ -182,6 +155,39 @@ describe 'Commits' do
end
end
end
+
+ describe '.gitlab-ci.yml not found warning' do
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'ci builds enabled' do
+ it 'does not show warning' do
+ visit pipeline_path(pipeline)
+
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+
+ it 'shows warning' do
+ stub_ci_pipeline_yaml_file(nil)
+
+ visit pipeline_path(pipeline)
+
+ expect(page).to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+
+ context 'ci builds disabled' do
+ it 'does not show warning' do
+ stub_ci_builds_disabled
+ stub_ci_pipeline_yaml_file(nil)
+
+ visit pipeline_path(pipeline)
+
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+ end
end
context 'viewing commits for a branch' do
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index 6a0cd848345..d31df322d10 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Archived Project' do
expect(page).not_to have_content(project.name)
end
- it 'searchs archived projects', :js do
+ it 'searches archived projects', :js do
click_button 'Last updated'
click_link 'Show archived projects'
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index e57fcde8b2c..259f220c68b 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -14,15 +14,15 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do
visit dashboard_groups_path
find('.btn-success').click
- new_path = 'Samurai'
+ new_name = 'Samurai'
new_description = 'Tokugawa Shogunate'
- fill_in 'group_path', with: new_path
+ fill_in 'group_name', with: new_name
fill_in 'group_description', with: new_description
click_button 'Create group'
- expect(current_path).to eq group_path(Group.find_by(name: new_path))
- expect(page).to have_content(new_path)
+ expect(current_path).to eq group_path(Group.find_by(name: new_name))
+ expect(page).to have_content(new_name)
expect(page).to have_content(new_description)
end
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 975b7944741..0a24c5e906a 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -147,10 +147,12 @@ 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: 'feature', action: :created)
+ create(:push_event_payload, event: event, ref: ref, action: :created)
Users::LastPushEventService.new(user).cache_last_push_event(event)
@@ -165,9 +167,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)
+ expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref)
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 'feature'
+ expect(find('input#merge_request_source_branch', visible: false).value).to eq ref
expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end
end
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 11f05b6d220..259f22139ef 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -29,7 +29,7 @@ describe 'Top Plus Menu', :js do
click_topmenuitem("New group")
- expect(page).to have_content('Group path')
+ expect(page).to have_content('Group URL')
expect(page).to have_content('Group name')
end
@@ -79,7 +79,7 @@ describe 'Top Plus Menu', :js do
click_topmenuitem("New subgroup")
- expect(page).to have_content('Group path')
+ expect(page).to have_content('Group URL')
expect(page).to have_content('Group name')
end
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
new file mode 100644
index 00000000000..9f597efa7b7
--- /dev/null
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Group Issue Boards', :js do
+ include BoardHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:group_member, user: create(:user), group: group ).user }
+ let!(:project_1) { create(:project, :public, group: group) }
+ let!(:project_2) { create(:project, :public, group: group) }
+ let!(:project_1_label) { create(:label, project: project_1, name: 'Development 1') }
+ let!(:project_2_label) { create(:label, project: project_2, name: 'Development 2') }
+ let!(:group_label) { create(:group_label, title: 'Bug', description: 'Fusce consequat', group: group) }
+ let!(:issue_1) { create(:labeled_issue, project: project_1, relative_position: 1) }
+ let!(:issue_2) { create(:labeled_issue, project: project_2, relative_position: 2) }
+ let(:board) { create(:board, group: group) }
+ let!(:list) { create(:list, board: board, label: project_1_label, position: 0) }
+ let(:card) { find('.board:nth-child(1)').first('.board-card') }
+
+ before do
+ sign_in(user)
+
+ visit group_board_path(group, board)
+ wait_for_requests
+ end
+
+ context 'labels' do
+ it 'only shows valid labels for the issue project and group' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ page.within('.selectbox') do
+ expect(page).to have_content(project_1_label.title)
+ expect(page).to have_content(group_label.title)
+ expect(page).not_to have_content(project_2_label.title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index e8ca6a6714f..174840794ed 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -95,9 +95,9 @@ describe 'Group milestones' do
end
it 'counts milestones correctly' do
- expect(find('.top-area .active .badge').text).to eq("2")
- expect(find('.top-area .closed .badge').text).to eq("2")
- expect(find('.top-area .all .badge').text).to eq("4")
+ expect(find('.top-area .active .badge').text).to eq("3")
+ expect(find('.top-area .closed .badge').text).to eq("3")
+ expect(find('.top-area .all .badge').text).to eq("6")
end
it 'lists legacy group milestones and group milestones' do
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 63aa26cf5fd..4d04b8043ec 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -7,7 +7,7 @@ describe 'Group' do
matcher :have_namespace_error_message do
match do |page|
- page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
+ page.has_content?("Group URL can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
end
end
@@ -18,7 +18,7 @@ describe 'Group' do
describe 'with space in group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'space group'
+ fill_in 'Group URL', with: 'space group'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -28,7 +28,7 @@ describe 'Group' do
describe 'with .atom at end of group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'atom_group.atom'
+ fill_in 'Group URL', with: 'atom_group.atom'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -38,7 +38,7 @@ describe 'Group' do
describe 'with .git at end of group path' do
it 'renders new group form with validation errors' do
- fill_in 'Group path', with: 'git_group.git'
+ fill_in 'Group URL', with: 'git_group.git'
click_button 'Create group'
expect(current_path).to eq(groups_path)
@@ -94,7 +94,8 @@ describe 'Group' do
end
it 'creates a nested group' do
- fill_in 'Group path', with: 'bar'
+ fill_in 'Group name', with: 'bar'
+ fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
@@ -112,7 +113,8 @@ describe 'Group' do
visit new_group_path(group, parent_id: group.id)
- fill_in 'Group path', with: 'bar'
+ fill_in 'Group name', with: 'bar'
+ fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index e381d073804..a90cdd8d920 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -22,7 +22,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js, :post
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
- it 'imports succesfully imports a project' do
+ it 'imports successfully imports a project' do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index d011d2545bb..e910fb54d23 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do
expect_filtered_search_input_empty
end
- it 'selects `no assignee`' do
- find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
+ it 'selects `None`' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('none')])
expect_filtered_search_input_empty
end
+
+ it 'selects `Any`' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect_tokens([assignee_token('any')])
+ expect_filtered_search_input_empty
+ end
end
describe 'selecting from dropdown without Ajax call' do
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index be229e8aa7d..c42fcd92a36 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -92,7 +92,7 @@ describe 'Dropdown emoji', :js do
it 'shows the most populated emoji at top of dropdown' do
send_keys_to_filtered_search('my-reaction:')
- expect(first('#js-dropdown-my-reaction li')).to have_content(award_emoji_star.name)
+ expect(first('#js-dropdown-my-reaction .filter-dropdown li')).to have_content(award_emoji_star.name)
end
end
@@ -121,13 +121,29 @@ describe 'Dropdown emoji', :js do
send_keys_to_filtered_search(':')
end
+ it 'selects `None`' do
+ find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ expect_tokens([reaction_token('none', false)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'selects `Any`' do
+ find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ expect_tokens([reaction_token('any', false)])
+ expect_filtered_search_input_empty
+ end
+
it 'fills in the my-reaction name' do
click_emoji('thumbsup')
wait_for_requests
expect(page).to have_css(js_dropdown_emoji, visible: false)
- expect_tokens([emoji_token('thumbsup')])
+ expect_tokens([reaction_token('thumbsup')])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 6ac7ccd00f7..1e1dd5691ab 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -118,7 +118,7 @@ describe 'Visual tokens', :js do
describe 'selecting static option from dropdown' do
before do
- find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+ find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
end
it 'changes value in visual token' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 08bf9bc7243..605860b90cd 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -15,7 +15,7 @@ describe 'GFM autocomplete', :js do
wait_for_requests
end
- it 'updates issue descripton with GFM reference' do
+ it 'updates issue description with GFM reference' do
find('.js-issuable-edit').click
simulate_input('#issue-description', "@#{user.name[0...3]}")
@@ -35,6 +35,21 @@ describe 'GFM autocomplete', :js do
expect(page).to have_selector('.atwho-container')
end
+ it 'opens autocomplete menu when field starts with text with item escaping HTML characters' do
+ alert_title = 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;'
+ create(:issue, project: project, title: alert_title)
+
+ page.within '.timeline-content-form' do
+ find('#note-body').native.send_keys('#')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ page.within '.atwho-container #at-view-issues' do
+ expect(page.all('li').first.text).to include(alert_title)
+ end
+ end
+
it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('testing')
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 0ccab5b2fac..a124c99ecc8 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_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index f744d7941f5..a298ead43db 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -3,15 +3,19 @@ require 'rails_helper'
describe 'Merge request > User sees deployment widget', :js do
describe 'when deployed to an environment' do
let(:user) { create(:user) }
- let(:project) { merge_request.target_project }
- let(:merge_request) { create(:merge_request, :merged) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:environment) { create(:environment, project: project) }
let(:role) { :developer }
- let(:sha) { project.commit('master').id }
- let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+ 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, 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)
@@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do
end
context 'with stop action' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- let(:deployment) do
- create(:deployment, environment: environment, ref: merge_request.target_branch,
- sha: sha, deployable: build, on_stop: 'close_app')
- end
before do
+ deployment.update!(on_stop: manual.name)
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 f15129759de..0c610edd6d1 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
@@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request' do
let!(:environment) { create(:environment, project: project) }
+ let(:sha) { project.commit(merge_request.source_branch).sha }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+ let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) do
create(:deployment, environment: environment,
- ref: 'feature',
- sha: merge_request.diff_head_sha)
+ ref: merge_request.source_branch,
+ deployable: build,
+ sha: sha)
end
before do
+ merge_request.update!(head_pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
end
it 'shows environments link' do
wait_for_requests
- page.within('.mr-widget-heading') do
+ page.within('.js-pre-merge-deploy') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
@@ -174,7 +179,7 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again')
+ expect(page).to have_text(%r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>})
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 45cccbee63e..41f447fba95 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -41,8 +41,8 @@ describe 'Merge request > User sees pipelines', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page.find('.ci-widget')).to have_content(
- 'Could not connect to the CI server. Please check your settings and try again')
+ expect(page.find('.ci-widget')).to have_text(
+ %r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>})
end
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 92cc73ddf1f..6dfc819fe8a 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 ae41cf90576..147544740dc 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: { target_branch: 'master', source_branch: 'fix' })
+ visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
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: { target_branch: 'master', source_branch: 'fix' })
+ visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
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: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
+ visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' })
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: { target_branch: 'master', source_branch: 'deleted-image-test' })
+ visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' })
click_link "Changes"
@@ -165,7 +165,8 @@ 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: { target_branch: 'master', source_branch: 'fix' })
+ merge_request_source_branch: 'fix',
+ merge_request: { target_branch: 'master' })
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 b81478a481f..6e681185e1f 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 ec1153b7f7f..8ecdec491b8 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: { target_branch: 'master', source_branch: source_branch })
+ visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
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: { target_branch: 'master', source_branch: source_branch })
+ visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
click_on 'Submit merge request'
wait_for_requests
end
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index 8c4488b2ca6..dee81898928 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -19,7 +19,7 @@ describe 'Pipeline Badge' do
let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: ref, sha: project.commit(ref).sha) }
let!(:job) { create(:ci_build, pipeline: pipeline) }
- context 'when the pipeline was successfull' do
+ context 'when the pipeline was successful' do
it 'displays so on the badge' do
job.success
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 8b92b9fc869..3d17eb3a73a 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -130,6 +130,7 @@ describe 'Gcp Cluster', :js do
context 'when user changes cluster parameters' do
before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
page.within('#js-cluster-details') { click_button 'Save changes' }
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 9ae1dba60b5..250c964cc32 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -9,7 +9,9 @@ describe 'User Cluster', :js do
before do
project.add_maintainer(user)
gitlab_sign_in(user)
+
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
end
context 'when user does not have a cluster and visits cluster index page' do
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 91eac9c8278..f13c35c00d3 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -40,7 +40,7 @@ describe 'Clusters', :js do
expect(page).to have_selector('.js-project-feature-toggle')
end
- context 'with sucessfull request' do
+ context 'with successful request' do
it 'user sees updated cluster' do
expect do
page.find('.js-project-feature-toggle').click
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 847b5f0860f..6f620dff82b 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))
+ expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature"))
end
end
@@ -80,8 +80,7 @@ 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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
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 d4dda43c823..14b5bd58bd1 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -16,7 +16,7 @@ describe 'Projects > Files > User creates files' do
sign_in(user)
end
- context 'without commiting a new file' do
+ context 'without committing a new file' do
context 'when an user has write access' do
before do
visit(project_tree_path_root_ref)
@@ -49,7 +49,7 @@ describe 'Projects > Files > User creates files' do
end
end
- context 'with commiting a new file' do
+ context 'with committing a new file' do
context 'when an user has write access' do
before do
visit(project_tree_path_root_ref)
@@ -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))
+ expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
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 614b11fa5c8..faf11ee9dd8 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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
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 9eb65ec159c..c6b2aaea906 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))
+ expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
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 e3da28d73c3..09feb315465 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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
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 af3fc528a20..92df8303f46 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))
+ expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
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))
+ expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
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 b3bea92e635..5cb3f7c732f 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -198,6 +198,24 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
+ context 'when job is running', :js do
+ let(:job) { create(:ci_build, :running, pipeline: pipeline) }
+ let(:job_url) { project_job_path(project, job) }
+
+ before do
+ visit job_url
+ wait_for_requests
+ end
+
+ context 'job is cancelable' do
+ it 'shows cancel button' do
+ click_link 'Cancel'
+
+ expect(page.current_path).to eq(job_url)
+ end
+ end
+ end
+
context "Job from other project" do
before do
visit project_job_path(project, job2)
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 69561b4d733..4be31511ceb 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',
- target_branch: 'master' })
+ merge_request_source_branch: 'feature',
+ merge_request: { 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',
- target_branch: 'master' })
+ merge_request_source_branch: 'feature',
+ merge_request: { target_branch: 'master' })
visit fork_url
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index fb766addb31..0add129dde2 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -322,6 +322,22 @@ describe 'Project' do
end
end
+ context 'content is not cached after signing out', :js do
+ let(:user) { create(:user, project_view: 'activity') }
+ let(:project) { create(:project, :repository) }
+
+ it 'does not load activity', :js do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit project_path(project)
+ sign_out(user)
+
+ page.evaluate_script('window.history.back()')
+
+ expect(page).not_to have_selector('.event-item')
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 3ee753b7d23..7225ca65492 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'User searches for wiki pages', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
before do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 2f164ffa8b0..c0488c83bd8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -144,19 +144,31 @@ describe IssuesFinder do
end
context 'filtering by no milestone' do
- let(:params) { { milestone_title: Milestone::None.title } }
+ let(:params) { { milestone_title: 'None' } }
it 'returns issues with no milestone' do
expect(issues).to contain_exactly(issue2, issue3, issue4)
end
+
+ it 'returns issues with no milestone (deprecated)' do
+ params[:milestone_title] = Milestone::None.title
+
+ expect(issues).to contain_exactly(issue2, issue3, issue4)
+ end
end
context 'filtering by any milestone' do
- let(:params) { { milestone_title: Milestone::Any.title } }
+ let(:params) { { milestone_title: 'Any' } }
it 'returns issues with any assigned milestone' do
expect(issues).to contain_exactly(issue1)
end
+
+ it 'returns issues with any assigned milestone (deprecated)' do
+ params[:milestone_title] = Milestone::Any.title
+
+ expect(issues).to contain_exactly(issue1)
+ end
end
context 'filtering by upcoming milestone' do
@@ -360,6 +372,22 @@ describe IssuesFinder do
end
context 'filtering by reaction name' do
+ context 'user searches by no reaction' do
+ let(:params) { { my_reaction_emoji: 'None' } }
+
+ it 'returns issues that the user did not react to' do
+ expect(issues).to contain_exactly(issue2, issue4)
+ end
+ end
+
+ context 'user searches by any reaction' do
+ let(:params) { { my_reaction_emoji: 'Any' } }
+
+ it 'returns issues that the user reacted to' do
+ expect(issues).to contain_exactly(issue1, issue3)
+ end
+ end
+
context 'user searches by "thumbsup" reaction' do
let(:params) { { my_reaction_emoji: 'thumbsup' } }
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index c40977bc4ee..35971d564d5 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -46,6 +46,7 @@
"diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
+ "merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 8833825e3fb..4878df43d28 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -15,6 +15,7 @@
"relative_position": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
+ "assignable_labels_endpoint": { "type": "string" },
"reference_path": { "type": "string" },
"real_path": { "type": "string" },
"project": {
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1c216b3fe97..f709f152c92 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -3,63 +3,13 @@ require 'spec_helper'
describe BlobHelper do
include TreeHelper
- let(:blob_name) { 'test.lisp' }
- let(:no_context_content) { ":type \"assem\"))" }
- let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
- let(:split_content) { blob_content.split("\n") }
- let(:multiline_content) do
- %q(
- def test(input):
- """This is line 1 of a multi-line comment.
- This is line 2.
- """
- )
- end
-
describe '#highlight' do
- it 'returns plaintext for unknown lexer context' do
- result = helper.highlight(blob_name, no_context_content)
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
+ it 'wraps highlighted content' do
+ expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>])
end
- it 'returns plaintext for long blobs' do
- stub_const('Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1)
- result = helper.highlight(blob_name, blob_content)
-
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span></code></pre>])
- end
-
- it 'highlights single block' do
- expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
-
- expect(helper.highlight(blob_name, blob_content)).to eq(expected)
- end
-
- it 'highlights multi-line comments' do
- result = helper.highlight(blob_name, multiline_content)
- html = Nokogiri::HTML(result)
- lines = html.search('.s')
- expect(lines.count).to eq(3)
- expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
- expect(lines[1].text).to eq(' This is line 2.')
- expect(lines[2].text).to eq(' """')
- end
-
- context 'diff highlighting' do
- let(:blob_name) { 'test.diff' }
- let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
- let(:expected) do
- %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
-<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
-<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
- end
-
- it 'highlights each line properly' do
- result = helper.highlight(blob_name, blob_content)
- expect(result).to eq(expected)
- end
+ it 'handles plain version' do
+ expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>])
end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index a2cda58e5d2..c04f679bcf0 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -211,4 +211,29 @@ describe LabelsHelper do
end
end
end
+
+ describe 'labels_filter_path' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project) }
+
+ it 'links to the dashboard labels page' do
+ expect(labels_filter_path).to eq(dashboard_labels_path)
+ end
+
+ it 'links to the group labels page' do
+ assign(:group, group)
+
+ expect(helper.labels_filter_path).to eq(group_labels_path(group))
+ end
+
+ it 'links to the project labels page' do
+ assign(:project, project)
+
+ expect(helper.labels_filter_path).to eq(project_labels_path(project))
+ end
+
+ it 'supports json format' do
+ expect(labels_filter_path(format: :json)).to eq(dashboard_labels_path(format: :json))
+ end
+ end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index b0689fc7cfe..ce5d2022441 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -219,7 +219,7 @@ describe('AwardsHandler', function() {
expect($thumbsUpEmoji.data('originalTitle')).toBe('You, sam, jerry, max, and andy');
});
- it('handles the special case where "You" is not cleanly comma seperated', function() {
+ it('handles the special case where "You" is not cleanly comma separated', function() {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
@@ -244,7 +244,7 @@ describe('AwardsHandler', function() {
expect($thumbsUpEmoji.data('originalTitle')).toBe('sam, jerry, max, and andy');
});
- it('handles the special case where "You" is not cleanly comma seperated', function() {
+ it('handles the special case where "You" is not cleanly comma separated', function() {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 037e06cf3b2..2642c8b1bdb 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -18,7 +18,7 @@ describe('Board list component', () => {
let mock;
let component;
- beforeEach((done) => {
+ beforeEach(done => {
const el = document.createElement('div');
document.body.appendChild(el);
@@ -62,122 +62,102 @@ describe('Board list component', () => {
});
it('renders component', () => {
- expect(
- component.$el.classList.contains('board-list-component'),
- ).toBe(true);
+ expect(component.$el.classList.contains('board-list-component')).toBe(true);
});
- it('renders loading icon', (done) => {
+ it('renders loading icon', done => {
component.loading = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-loading'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
done();
});
});
it('renders issues', () => {
- expect(
- component.$el.querySelectorAll('.board-card').length,
- ).toBe(1);
+ expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
});
it('sets data attribute with issue id', () => {
- expect(
- component.$el.querySelector('.board-card').getAttribute('data-issue-id'),
- ).toBe('1');
+ expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
});
- it('shows new issue form', (done) => {
+ it('shows new issue form', done => {
component.toggleForm();
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(
- component.$el.querySelector('.is-smaller'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
- it('shows new issue form after eventhub event', (done) => {
+ it('shows new issue form after eventhub event', done => {
eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(
- component.$el.querySelector('.is-smaller'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
done();
});
});
- it('does not show new issue form for closed list', (done) => {
+ it('does not show new issue form for closed list', done => {
component.list.type = 'closed';
component.toggleForm();
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-new-issue-form'),
- ).toBeNull();
+ expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
done();
});
});
- it('shows count list item', (done) => {
+ it('shows count list item', done => {
component.showCount = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
- expect(
- component.$el.querySelector('.board-list-count').textContent.trim(),
- ).toBe('Showing all issues');
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing all issues',
+ );
done();
});
});
- it('sets data attribute with invalid id', (done) => {
+ it('sets data attribute with invalid id', done => {
component.showCount = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'),
- ).toBe('-1');
+ expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
+ '-1',
+ );
done();
});
});
- it('shows how many more issues to load', (done) => {
+ it('shows how many more issues to load', done => {
component.showCount = true;
component.list.issuesSize = 20;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count').textContent.trim(),
- ).toBe('Showing 1 of 20 issues');
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing 1 of 20 issues',
+ );
done();
});
});
- it('loads more issues after scrolling', (done) => {
+ it('loads more issues after scrolling', done => {
spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll';
@@ -200,7 +180,9 @@ describe('Board list component', () => {
});
it('does not load issues if already loading', () => {
- component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {}));
+ component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
+ new Promise(() => {}),
+ );
component.onScroll();
component.onScroll();
@@ -208,14 +190,12 @@ describe('Board list component', () => {
expect(component.list.nextPage).toHaveBeenCalledTimes(1);
});
- it('shows loading more spinner', (done) => {
+ it('shows loading more spinner', done => {
component.showCount = true;
component.list.loadingMore = true;
Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-list-count .fa-spinner'),
- ).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull();
done();
});
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index d4c53bd5a7d..dee7841c088 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -8,7 +8,7 @@ describe('Board component', () => {
let vm;
let el;
- beforeEach((done) => {
+ beforeEach(done => {
loadFixtures('boards/show.html.raw');
el = document.createElement('div');
@@ -50,56 +50,46 @@ describe('Board component', () => {
});
it('board is expandable when list type is backlog', () => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(true);
});
- it('board is expandable when list type is closed', (done) => {
+ it('board is expandable when list type is closed', done => {
vm.list.type = 'closed';
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(true);
done();
});
});
- it('board is not expandable when list type is label', (done) => {
+ it('board is not expandable when list type is label', done => {
vm.list.type = 'label';
vm.list.isExpandable = false;
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-expandable'),
- ).toBe(false);
+ expect(vm.$el.classList.contains('is-expandable')).toBe(false);
done();
});
});
- it('collapses when clicking header', (done) => {
+ it('collapses when clicking header', done => {
vm.$el.querySelector('.board-header').click();
Vue.nextTick(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
done();
});
});
- it('created sets isExpanded to true from localStorage', (done) => {
+ it('created sets isExpanded to true from localStorage', done => {
vm.$el.querySelector('.board-header').click();
return Vue.nextTick()
.then(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
// call created manually
vm.$options.created[0].call(vm);
@@ -107,11 +97,10 @@ describe('Board component', () => {
return Vue.nextTick();
})
.then(() => {
- expect(
- vm.$el.classList.contains('is-collapsed'),
- ).toBe(true);
+ expect(vm.$el.classList.contains('is-collapsed')).toBe(true);
done();
- }).catch(done.fail);
+ })
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index 4f8701bae01..1fc0e206d5e 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -24,7 +24,7 @@ describe('AjaxFormVariableList', () => {
mock = new MockAdapter(axios);
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
@@ -44,7 +44,7 @@ describe('AjaxFormVariableList', () => {
describe('onSaveClicked', () => {
it('shows loading spinner while waiting for the request', done => {
- const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
+ const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
@@ -172,7 +172,7 @@ describe('AjaxFormVariableList', () => {
container = document.querySelector('.js-ci-variable-list-section');
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
errorBox = container.querySelector('.js-ci-variable-error-box');
ajaxVariableList = new AjaxFormVariableList({
container,
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
index 4fc56fd9a27..f6b36e88a5f 100644
--- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -22,7 +22,7 @@ describe('Commit pipeline status component', () => {
Component = Vue.extend(commitPipelineStatus);
});
- describe('While polling pipeline data succesfully', () => {
+ describe('While polling pipeline data successfully', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/dummy/endpoint').reply(() => {
@@ -59,14 +59,14 @@ describe('Commit pipeline status component', () => {
});
});
- it('contains a ciStatus when the polling is succesful ', done => {
+ it('contains a ciStatus when the polling is successful ', done => {
setTimeout(() => {
expect(vm.ciStatus).toEqual(mockCiStatus);
done();
});
});
- it('contains a ci-status icon when polling is succesful', done => {
+ it('contains a ci-status icon when polling is successful', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null);
expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(
@@ -77,7 +77,7 @@ describe('Commit pipeline status component', () => {
});
});
- describe('When polling data was not succesful', () => {
+ describe('When polling data was not successful', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/dummy/endpoint').reply(502, {});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index b797cc44ae7..04c8ab44405 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -72,6 +72,29 @@ describe('Pipelines table in Commits and Merge requests', function() {
done();
}, 0);
});
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', done => {
+ setTimeout(() => {
+ spyOn(vm, 'updateContent');
+
+ vm.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-next-button a').click();
+
+ expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' });
+ done();
+ });
+ });
+ });
+ });
});
describe('pipeline badge counts', () => {
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index 7237274eb43..d9d7f61785f 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -1 +1,125 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+import Vue from 'vue';
+import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
+import store from '~/mr_notes/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffsMockData from '../mock_data/merge_request_diffs';
+
+describe('CompareVersions', () => {
+ let vm;
+ const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, {
+ mergeRequestDiffs: diffsMockData,
+ mergeRequestDiff: diffsMockData[0],
+ targetBranch,
+ }).$mount();
+ });
+
+ describe('template', () => {
+ it('should render Tree List toggle button with correct attribute values', () => {
+ const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list');
+
+ expect(treeListBtn).not.toBeNull();
+ expect(treeListBtn.dataset.originalTitle).toBe('Toggle file browser');
+ expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0);
+ expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain(
+ '#hamburger',
+ );
+ });
+
+ it('should render comparison dropdowns with correct values', () => {
+ const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
+ const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown).not.toBeNull();
+ expect(targetDropdown).not.toBeNull();
+ expect(sourceDropdown.querySelector('a span').innerHTML).toContain('latest version');
+ expect(targetDropdown.querySelector('a span').innerHTML).toContain(targetBranch.branchName);
+ });
+
+ it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
+ vm.mergeRequestDiffs = [];
+
+ vm.$nextTick(() => {
+ const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
+ const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown).toBeNull();
+ expect(targetDropdown).toBeNull();
+ });
+ });
+
+ it('should render whitespace toggle button with correct attributes', () => {
+ const whitespaceBtn = vm.$el.querySelector('.qa-toggle-whitespace');
+ const href = vm.toggleWhitespacePath;
+
+ expect(whitespaceBtn).not.toBeNull();
+ expect(whitespaceBtn.getAttribute('href')).toEqual(href);
+ expect(whitespaceBtn.innerHTML).toContain('Hide whitespace changes');
+ });
+
+ it('should render view types buttons with correct values', () => {
+ const inlineBtn = vm.$el.querySelector('#inline-diff-btn');
+ const parallelBtn = vm.$el.querySelector('#parallel-diff-btn');
+
+ expect(inlineBtn).not.toBeNull();
+ expect(parallelBtn).not.toBeNull();
+ expect(inlineBtn.dataset.viewType).toEqual('inline');
+ expect(parallelBtn.dataset.viewType).toEqual('parallel');
+ expect(inlineBtn.innerHTML).toContain('Inline');
+ expect(parallelBtn.innerHTML).toContain('Side-by-side');
+ });
+ });
+
+ describe('setInlineDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = vm.$el.querySelector('#inline-diff-btn');
+ viewTypeBtn.click();
+
+ expect(window.location.toString()).toContain('?view=inline');
+ });
+ });
+
+ describe('setParallelDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = vm.$el.querySelector('#parallel-diff-btn');
+ viewTypeBtn.click();
+
+ expect(window.location.toString()).toContain('?view=parallel');
+ });
+ });
+
+ describe('comparableDiffs', () => {
+ it('should not contain the first item in the mergeRequestDiffs property', () => {
+ const { comparableDiffs } = vm;
+ const comparableDiffsMock = diffsMockData.slice(1);
+
+ expect(comparableDiffs).toEqual(comparableDiffsMock);
+ });
+ });
+
+ describe('isWhitespaceVisible', () => {
+ const originalHref = window.location.href;
+
+ afterEach(() => {
+ window.history.replaceState({}, null, originalHref);
+ });
+
+ it('should return "true" when no "w" flag is present in the URL (default)', () => {
+ expect(vm.isWhitespaceVisible()).toBe(true);
+ });
+
+ it('should return "false" when the flag is set to "1" in the URL', () => {
+ window.history.replaceState({}, null, '?w=1');
+
+ expect(vm.isWhitespaceVisible()).toBe(false);
+ });
+
+ it('should return "true" when the flag is set to "0" in the URL', () => {
+ window.history.replaceState({}, null, '?w=0');
+
+ expect(vm.isWhitespaceVisible()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
new file mode 100644
index 00000000000..d72ad7818dd
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
@@ -0,0 +1,42 @@
+export default [
+ {
+ versionIndex: 4,
+ createdAt: '2018-10-23T11:49:16.611Z',
+ commitsCount: 4,
+ latest: true,
+ shortCommitSha: 'de7a8f7f',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995',
+ },
+ {
+ versionIndex: 3,
+ createdAt: '2018-10-23T11:46:40.617Z',
+ commitsCount: 3,
+ latest: false,
+ shortCommitSha: 'e78fc18f',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e',
+ },
+ {
+ versionIndex: 2,
+ createdAt: '2018-10-04T09:57:39.648Z',
+ commitsCount: 2,
+ latest: false,
+ shortCommitSha: '48da7e7e',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e',
+ },
+ {
+ versionIndex: 1,
+ createdAt: '2018-09-25T20:30:39.493Z',
+ commitsCount: 1,
+ latest: false,
+ shortCommitSha: '47bac2ed',
+ versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
+ comparePath:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0',
+ },
+];
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 4b6d3d5bcba..fed04cbaed8 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -221,6 +221,7 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1);
expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1);
+ expect(state.diffFiles[0].parallelDiffLines[0].right.discussions).toEqual([]);
expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1);
expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1);
diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js
index d71dfe8197e..1f986d49bc7 100644
--- a/spec/javascripts/environments/emtpy_state_spec.js
+++ b/spec/javascripts/environments/emtpy_state_spec.js
@@ -1,4 +1,3 @@
-
import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -25,13 +24,13 @@ describe('environments empty state', () => {
});
it('renders empty state and new environment button', () => {
- expect(
- vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
- ).toEqual('You don\'t have any environments right now');
+ expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
+ "You don't have any environments right now",
+ );
- expect(
- vm.$el.querySelector('.js-new-environment-button').getAttribute('href'),
- ).toEqual('foo');
+ expect(vm.$el.querySelector('.js-new-environment-button').getAttribute('href')).toEqual(
+ 'foo',
+ );
});
});
@@ -45,13 +44,11 @@ describe('environments empty state', () => {
});
it('renders empty state without new button', () => {
- expect(
- vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
- ).toEqual('You don\'t have any environments right now');
+ expect(vm.$el.querySelector('.js-blank-state-title').textContent.trim()).toEqual(
+ "You don't have any environments right now",
+ );
- expect(
- vm.$el.querySelector('.js-new-environment-button'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.js-new-environment-button')).toBeNull();
});
});
});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 0b933dda431..7618c2f50ce 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -38,7 +38,9 @@ describe('Environment item', () => {
});
it('Should render the number of children in a badge', () => {
- expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(
+ mockItem.size,
+ );
});
});
@@ -68,7 +70,8 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
@@ -84,7 +87,8 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
@@ -121,18 +125,18 @@ describe('Environment item', () => {
});
it('should render environment name', () => {
- expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
+ expect(component.$el.querySelector('.environment-name').textContent).toContain(
+ environment.name,
+ );
});
describe('With deployment', () => {
it('should render deployment internal id', () => {
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain(environment.last_deployment.iid);
+ expect(component.$el.querySelector('.deployment-column span').textContent).toContain(
+ environment.last_deployment.iid,
+ );
- expect(
- component.$el.querySelector('.deployment-column span').textContent,
- ).toContain('#');
+ expect(component.$el.querySelector('.deployment-column span').textContent).toContain('#');
});
it('should render last deployment date', () => {
@@ -156,56 +160,46 @@ describe('Environment item', () => {
describe('With build url', () => {
it('Should link to build url provided', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
+ expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
+ environment.last_deployment.deployable.build_path,
+ );
});
it('Should render deployable name and id', () => {
- expect(
- component.$el.querySelector('.build-link').getAttribute('href'),
- ).toEqual(environment.last_deployment.deployable.build_path);
+ expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
+ environment.last_deployment.deployable.build_path,
+ );
});
});
describe('With commit information', () => {
it('should render commit component', () => {
- expect(
- component.$el.querySelector('.js-commit-component'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-commit-component')).toBeDefined();
});
});
});
describe('With manual actions', () => {
it('Should render actions component', () => {
- expect(
- component.$el.querySelector('.js-manual-actions-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined();
});
});
describe('With external URL', () => {
it('should render external url component', () => {
- expect(
- component.$el.querySelector('.js-external-url-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-external-url-container')).toBeDefined();
});
});
describe('With stop action', () => {
it('Should render stop action component', () => {
- expect(
- component.$el.querySelector('.js-stop-component-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined();
});
});
describe('With retry action', () => {
it('Should render rollback component', () => {
- expect(
- component.$el.querySelector('.js-rollback-component-container'),
- ).toBeDefined();
+ expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined();
});
});
});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index 7edc0ccac0b..e2d81eb454a 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -31,9 +31,9 @@ describe('Environment', () => {
mock.restore();
});
- describe('successfull request', () => {
+ describe('successful request', () => {
describe('without environments', () => {
- beforeEach((done) => {
+ beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { environments: [] });
component = mountComponent(EnvironmentsComponent, mockData);
@@ -44,30 +44,34 @@ describe('Environment', () => {
});
it('should render the empty state', () => {
- expect(
- component.$el.querySelector('.js-new-environment-button').textContent,
- ).toContain('New environment');
+ expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
+ 'New environment',
+ );
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now');
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
});
});
describe('with paginated environments', () => {
- beforeEach((done) => {
- mock.onGet(mockData.endpoint).reply(200, {
- environments: [environment],
- stopped_count: 1,
- available_count: 0,
- }, {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- });
+ beforeEach(done => {
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ );
component = mountComponent(EnvironmentsComponent, mockData);
@@ -78,19 +82,17 @@ describe('Environment', () => {
it('should render a table with environments', () => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environment.name);
+ expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
+ environment.name,
+ );
});
describe('pagination', () => {
it('should render pagination', () => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
+ expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5);
});
- it('should make an API request when page is clicked', (done) => {
+ it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
@@ -100,7 +102,7 @@ describe('Environment', () => {
}, 0);
});
- it('should make an API request when using tabs', (done) => {
+ it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
@@ -114,7 +116,7 @@ describe('Environment', () => {
});
describe('unsuccessfull request', () => {
- beforeEach((done) => {
+ beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {});
component = mountComponent(EnvironmentsComponent, mockData);
@@ -125,15 +127,16 @@ describe('Environment', () => {
});
it('should render empty state', () => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now');
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
});
});
describe('expandable folders', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200,
+ mock.onGet(mockData.endpoint).reply(
+ 200,
{
environments: [folder],
stopped_count: 0,
@@ -154,7 +157,7 @@ describe('Environment', () => {
component = mountComponent(EnvironmentsComponent, mockData);
});
- it('should open a closed folder', (done) => {
+ it('should open a closed folder', done => {
setTimeout(() => {
component.$el.querySelector('.folder-name').click();
@@ -165,7 +168,7 @@ describe('Environment', () => {
}, 0);
});
- it('should close an opened folder', (done) => {
+ it('should close an opened folder', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
@@ -182,7 +185,7 @@ describe('Environment', () => {
}, 0);
});
- it('should show children environments and a button to show all environments', (done) => {
+ it('should show children environments and a button to show all environments', done => {
setTimeout(() => {
// open folder
component.$el.querySelector('.folder-name').click();
@@ -191,7 +194,9 @@ describe('Environment', () => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
- expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
+ 'Show all',
+ );
done();
});
});
@@ -201,7 +206,8 @@ describe('Environment', () => {
describe('methods', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200,
+ mock.onGet(mockData.endpoint).reply(
+ 200,
{
environments: [],
stopped_count: 0,
@@ -215,8 +221,9 @@ describe('Environment', () => {
});
describe('updateContent', () => {
- it('should set given parameters', (done) => {
- component.updateContent({ scope: 'stopped', page: '3' })
+ it('should set given parameters', done => {
+ component
+ .updateContent({ scope: 'stopped', page: '3' })
.then(() => {
expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped');
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 51d4213c38f..7f0a9475d5f 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -30,39 +30,43 @@ describe('Environments Folder View', () => {
component.$destroy();
});
- describe('successfull request', () => {
+ describe('successful request', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(200, {
- environments: environmentsList,
- stopped_count: 1,
- available_count: 0,
- }, {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '20',
- 'X-Total-Pages': '10',
- });
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: environmentsList,
+ stopped_count: 1,
+ available_count: 0,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '20',
+ 'X-Total-Pages': '10',
+ },
+ );
component = mountComponent(Component, mockData);
});
- it('should render a table with environments', (done) => {
+ it('should render a table with environments', done => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
- expect(
- component.$el.querySelector('.environment-name').textContent.trim(),
- ).toEqual(environmentsList[0].name);
+ expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
+ environmentsList[0].name,
+ );
done();
}, 0);
});
- it('should render available tab with count', (done) => {
+ it('should render available tab with count', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-available').textContent,
- ).toContain('Available');
+ expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
+ 'Available',
+ );
expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent,
@@ -71,11 +75,11 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render stopped tab with count', (done) => {
+ it('should render stopped tab with count', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-stopped').textContent,
- ).toContain('Stopped');
+ expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
+ 'Stopped',
+ );
expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
@@ -84,36 +88,37 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render parent folder name', (done) => {
+ it('should render parent folder name', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-folder-name').textContent.trim(),
- ).toContain('Environments / review');
+ expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain(
+ 'Environments / review',
+ );
done();
}, 0);
});
describe('pagination', () => {
- it('should render pagination', (done) => {
+ it('should render pagination', done => {
setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination'),
- ).not.toBeNull();
+ expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull();
done();
}, 0);
});
- it('should make an API request when changing page', (done) => {
+ it('should make an API request when changing page', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
component.$el.querySelector('.gl-pagination .js-last-button a').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' });
+ expect(component.updateContent).toHaveBeenCalledWith({
+ scope: component.scope,
+ page: '10',
+ });
done();
}, 0);
});
- it('should make an API request when using tabs', (done) => {
+ it('should make an API request when using tabs', done => {
setTimeout(() => {
spyOn(component, 'updateContent');
component.$el.querySelector('.js-environments-tab-stopped').click();
@@ -134,20 +139,18 @@ describe('Environments Folder View', () => {
component = mountComponent(Component, mockData);
});
- it('should not render a table', (done) => {
+ it('should not render a table', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('table'),
- ).toBe(null);
+ expect(component.$el.querySelector('table')).toBe(null);
done();
}, 0);
});
- it('should render available tab with count 0', (done) => {
+ it('should render available tab with count 0', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-available').textContent,
- ).toContain('Available');
+ expect(component.$el.querySelector('.js-environments-tab-available').textContent).toContain(
+ 'Available',
+ );
expect(
component.$el.querySelector('.js-environments-tab-available .badge').textContent,
@@ -156,11 +159,11 @@ describe('Environments Folder View', () => {
}, 0);
});
- it('should render stopped tab with count 0', (done) => {
+ it('should render stopped tab with count 0', done => {
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-environments-tab-stopped').textContent,
- ).toContain('Stopped');
+ expect(component.$el.querySelector('.js-environments-tab-stopped').textContent).toContain(
+ 'Stopped',
+ );
expect(
component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
@@ -181,8 +184,9 @@ describe('Environments Folder View', () => {
});
describe('updateContent', () => {
- it('should set given parameters', (done) => {
- component.updateContent({ scope: 'stopped', page: '4' })
+ it('should set given parameters', done => {
+ component
+ .updateContent({ scope: 'stopped', page: '4' })
.then(() => {
expect(component.page).toEqual('4');
expect(component.scope).toEqual('stopped');
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
index 9e6f236b687..9754c8a6755 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -25,25 +25,21 @@ describe('Title component', () => {
});
it('renders title HTML', () => {
- expect(
- vm.$el.querySelector('.title').innerHTML.trim(),
- ).toBe('Testing <img>');
+ expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
- it('updates page title when changing titleHtml', (done) => {
+ it('updates page title when changing titleHtml', done => {
spyOn(vm, 'setPageTitle');
vm.titleHtml = 'test';
Vue.nextTick(() => {
- expect(
- vm.setPageTitle,
- ).toHaveBeenCalled();
+ expect(vm.setPageTitle).toHaveBeenCalled();
done();
});
});
- it('animates title changes', (done) => {
+ it('animates title changes', done => {
vm.titleHtml = 'test';
Vue.nextTick(() => {
@@ -61,14 +57,12 @@ describe('Title component', () => {
});
});
- it('updates page title after changing title', (done) => {
+ it('updates page title after changing title', done => {
vm.titleHtml = 'changed';
vm.titleText = 'changed';
Vue.nextTick(() => {
- expect(
- document.querySelector('title').textContent.trim(),
- ).toContain('changed');
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
done();
});
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
deleted file mode 100644
index bc973407b25..00000000000
--- a/spec/javascripts/job_spec.js
+++ /dev/null
@@ -1,265 +0,0 @@
-// import $ from 'jquery';
-// import MockAdapter from 'axios-mock-adapter';
-// import axios from '~/lib/utils/axios_utils';
-// import { numberToHumanSize } from '~/lib/utils/number_utils';
-// import '~/lib/utils/datetime_utility';
-// import Job from '~/job';
-// import '~/breakpoints';
-// import waitForPromises from 'spec/helpers/wait_for_promises';
-
-// describe('Job', () => {
-// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
-// let mock;
-// let response;
-// let job;
-
-// preloadFixtures('builds/build-with-artifacts.html.raw');
-
-// beforeEach(() => {
-// loadFixtures('builds/build-with-artifacts.html.raw');
-
-// spyOnDependency(Job, 'visitUrl');
-
-// response = {};
-
-// mock = new MockAdapter(axios);
-
-// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
-// });
-
-// afterEach(() => {
-// mock.restore();
-
-// clearTimeout(job.timeout);
-// });
-
-// describe('class constructor', () => {
-// beforeEach(() => {
-// jasmine.clock().install();
-// });
-
-// afterEach(() => {
-// jasmine.clock().uninstall();
-// });
-
-// describe('running build', () => {
-// it('updates the build trace on an interval', function (done) {
-// response = {
-// html: '<span>Update<span>',
-// status: 'running',
-// state: 'newstate',
-// append: true,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-// expect(job.state).toBe('newstate');
-
-// response = {
-// html: '<span>More</span>',
-// status: 'running',
-// state: 'finalstate',
-// append: true,
-// complete: true,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
-// expect(job.state).toBe('finalstate');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('replaces the entire build trace', (done) => {
-// response = {
-// html: '<span>Update<span>',
-// status: 'running',
-// append: false,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
-// response = {
-// html: '<span>Different</span>',
-// status: 'running',
-// append: false,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
-// expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-// });
-
-// describe('truncated information', () => {
-// describe('when size is less than total', () => {
-// it('shows information about truncated log', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('shows the size in KiB', (done) => {
-// const size = 50;
-
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(size)}`);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('shows incremented size', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// complete: false,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(50)}`);
-
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: true,
-// size: 10,
-// total: 100,
-// complete: true,
-// };
-// })
-// .then(() => jasmine.clock().tick(4001))
-// .then(waitForPromises)
-// .then(() => {
-// expect(
-// document.querySelector('.js-truncated-info-size').textContent.trim(),
-// ).toEqual(`${numberToHumanSize(60)}`);
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('renders the raw link', () => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// expect(
-// document.querySelector('.js-raw-link').textContent.trim(),
-// ).toContain('Complete Raw');
-// });
-// });
-
-// describe('when size is equal than total', () => {
-// it('does not show the trunctated information', (done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 100,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(() => {
-// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
-// })
-// .then(done)
-// .catch(done.fail);
-// });
-// });
-// });
-
-// describe('output trace', () => {
-// beforeEach((done) => {
-// response = {
-// html: '<span>Update</span>',
-// status: 'success',
-// append: false,
-// size: 50,
-// total: 100,
-// };
-
-// job = new Job();
-
-// waitForPromises()
-// .then(done)
-// .catch(done.fail);
-// });
-
-// it('should render trace controls', () => {
-// const controllers = document.querySelector('.controllers');
-
-// expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
-// expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
-// expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
-// });
-
-// it('should render received output', () => {
-// expect(
-// document.querySelector('.js-build-output').innerHTML,
-// ).toEqual('<span>Update</span>');
-// });
-// });
-// });
-
-// });
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index 288c06d6615..ba1889c4dcd 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -52,7 +52,7 @@ describe('Job App ', () => {
});
});
- describe('with successfull request', () => {
+ describe('with successful request', () => {
beforeEach(() => {
mock.onGet(`${props.pagePath}/trace.json`).replyOnce(200, {});
});
@@ -234,7 +234,7 @@ describe('Job App ', () => {
);
done();
}, 0);
- })
+ });
});
it('does not renders stuck block when there are no runners', done => {
diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js
index 45130b983e7..77b44995b12 100644
--- a/spec/javascripts/jobs/store/actions_spec.js
+++ b/spec/javascripts/jobs/store/actions_spec.js
@@ -68,41 +68,20 @@ describe('Job State actions', () => {
describe('hideSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(
- hideSidebar,
- null,
- mockedState,
- [{ type: types.HIDE_SIDEBAR }],
- [],
- done,
- );
+ testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
});
});
describe('showSidebar', () => {
it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(
- showSidebar,
- null,
- mockedState,
- [{ type: types.SHOW_SIDEBAR }],
- [],
- done,
- );
+ testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
});
});
describe('toggleSidebar', () => {
describe('when isSidebarOpen is true', () => {
it('should dispatch hideSidebar', done => {
- testAction(
- toggleSidebar,
- null,
- mockedState,
- [],
- [{ type: 'hideSidebar' }],
- done,
- );
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
});
});
@@ -110,14 +89,7 @@ describe('Job State actions', () => {
it('should dispatch showSidebar', done => {
mockedState.isSidebarOpen = false;
- testAction(
- toggleSidebar,
- null,
- mockedState,
- [],
- [{ type: 'showSidebar' }],
- done,
- );
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
});
});
});
diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js
index 34e9707eadd..4195d9d3680 100644
--- a/spec/javascripts/jobs/store/getters_spec.js
+++ b/spec/javascripts/jobs/store/getters_spec.js
@@ -180,7 +180,7 @@ describe('Job Store Getters', () => {
it('returns true', () => {
localState.job.runners = {
available: true,
- online: false
+ online: false,
};
expect(getters.hasRunnersForProject(localState)).toEqual(true);
@@ -191,7 +191,7 @@ describe('Job Store Getters', () => {
it('returns false', () => {
localState.job.runners = {
available: false,
- online: false
+ online: false,
};
expect(getters.hasRunnersForProject(localState)).toEqual(false);
@@ -202,7 +202,7 @@ describe('Job Store Getters', () => {
it('returns false', () => {
localState.job.runners = {
available: false,
- online: true
+ online: true,
};
expect(getters.hasRunnersForProject(localState)).toEqual(false);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 514d6ddeae5..0fb90c3b78c 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -35,9 +35,7 @@ describe('common_utils', () => {
});
it('should decode params', () => {
- expect(
- commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0],
- ).toBe('label_name[]=test');
+ expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test');
});
it('should remove the question mark from the search params', () => {
@@ -49,25 +47,19 @@ describe('common_utils', () => {
describe('urlParamsToObject', () => {
it('parses path for label with trailing +', () => {
- expect(
- commonUtils.urlParamsToObject('label_name[]=label%2B', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({
label_name: ['label+'],
});
});
it('parses path for milestone with trailing +', () => {
- expect(
- commonUtils.urlParamsToObject('milestone_title=A%2B', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({
milestone_title: 'A+',
});
});
it('parses path for search terms with spaces', () => {
- expect(
- commonUtils.urlParamsToObject('search=two+words', {}),
- ).toEqual({
+ expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({
search: 'two words',
});
});
@@ -187,7 +179,11 @@ describe('common_utils', () => {
describe('parseQueryStringIntoObject', () => {
it('should return object with query parameters', () => {
- expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' });
+ expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({
+ scope: 'all',
+ page: '2',
+ });
+
expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' });
expect(commonUtils.parseQueryStringIntoObject()).toEqual({});
});
@@ -211,7 +207,9 @@ describe('common_utils', () => {
describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
- expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`);
+ expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(
+ `${window.location.pathname}?page=2`,
+ );
});
});
@@ -266,21 +264,24 @@ describe('common_utils', () => {
});
describe('normalizeCRLFHeaders', () => {
- beforeEach(function () {
- this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
+ beforeEach(function() {
+ this.CLRFHeaders =
+ 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
spyOn(String.prototype, 'split').and.callThrough();
this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders);
});
- it('should split by newline', function () {
+ it('should split by newline', function() {
expect(String.prototype.split).toHaveBeenCalledWith('\n');
});
- it('should split by colon+space for each header', function () {
- expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
+ it('should split by colon+space for each header', function() {
+ expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(
+ 3,
+ );
});
- it('should return a normalized headers object', function () {
+ it('should return a normalized headers object', function() {
expect(this.normalizeCRLFHeaders).toEqual({
'A-HEADER': 'a-value',
'ANOTHER-HEADER': 'ANOTHER-VALUE',
@@ -359,67 +360,79 @@ describe('common_utils', () => {
spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0));
});
- it('solves the promise from the callback', (done) => {
+ it('solves the promise from the callback', done => {
const expectedResponseValue = 'Success!';
- commonUtils.backOff((next, stop) => (
- new Promise((resolve) => {
- resolve(expectedResponseValue);
- }).then((resp) => {
- stop(resp);
+ commonUtils
+ .backOff((next, stop) =>
+ new Promise(resolve => {
+ resolve(expectedResponseValue);
+ })
+ .then(resp => {
+ stop(resp);
+ })
+ .catch(done.fail),
+ )
+ .then(respBackoff => {
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
})
- ).catch(done.fail)).then((respBackoff) => {
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- }).catch(done.fail);
+ .catch(done.fail);
});
- it('catches the rejected promise from the callback ', (done) => {
+ it('catches the rejected promise from the callback ', done => {
const errorMessage = 'Mistakes were made!';
- commonUtils.backOff((next, stop) => {
- new Promise((resolve, reject) => {
- reject(new Error(errorMessage));
- }).then((resp) => {
- stop(resp);
- }).catch(err => stop(err));
- }).catch((errBackoffResp) => {
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe(errorMessage);
- done();
- });
+ commonUtils
+ .backOff((next, stop) => {
+ new Promise((resolve, reject) => {
+ reject(new Error(errorMessage));
+ })
+ .then(resp => {
+ stop(resp);
+ })
+ .catch(err => stop(err));
+ })
+ .catch(errBackoffResp => {
+ expect(errBackoffResp instanceof Error).toBe(true);
+ expect(errBackoffResp.message).toBe(errorMessage);
+ done();
+ });
});
- it('solves the promise correctly after retrying a third time', (done) => {
+ it('solves the promise correctly after retrying a third time', done => {
let numberOfCalls = 1;
const expectedResponseValue = 'Success!';
- commonUtils.backOff((next, stop) => (
- Promise.resolve(expectedResponseValue)
- .then((resp) => {
- if (numberOfCalls < 3) {
- numberOfCalls += 1;
- next();
- } else {
- stop(resp);
- }
- })
- ).catch(done.fail)).then((respBackoff) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ commonUtils
+ .backOff((next, stop) =>
+ Promise.resolve(expectedResponseValue)
+ .then(resp => {
+ if (numberOfCalls < 3) {
+ numberOfCalls += 1;
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ .catch(done.fail),
+ )
+ .then(respBackoff => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000]);
- expect(respBackoff).toBe(expectedResponseValue);
- done();
- }).catch(done.fail);
+ expect(timeouts).toEqual([2000, 4000]);
+ expect(respBackoff).toBe(expectedResponseValue);
+ done();
+ })
+ .catch(done.fail);
});
- it('rejects the backOff promise after timing out', (done) => {
- commonUtils.backOff(next => next(), 64000)
- .catch((errBackoffResp) => {
- const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
+ it('rejects the backOff promise after timing out', done => {
+ commonUtils.backOff(next => next(), 64000).catch(errBackoffResp => {
+ const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout);
- expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
- done();
- });
+ expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]);
+ expect(errBackoffResp instanceof Error).toBe(true);
+ expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
+ done();
+ });
});
});
@@ -466,11 +479,14 @@ describe('common_utils', () => {
});
describe('createOverlayIcon', () => {
- it('should return the favicon with the overlay', (done) => {
- commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => {
- expect(url).toEqual(faviconWithOverlayDataUrl);
- done();
- }).catch(done.fail);
+ it('should return the favicon with the overlay', done => {
+ commonUtils
+ .createOverlayIcon(faviconDataUrl, overlayDataUrl)
+ .then(url => {
+ expect(url).toEqual(faviconWithOverlayDataUrl);
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -486,11 +502,16 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should set page favicon to provided favicon overlay', (done) => {
- commonUtils.setFaviconOverlay(overlayDataUrl).then(() => {
- expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
- done();
- }).catch(done.fail);
+ it('should set page favicon to provided favicon overlay', done => {
+ commonUtils
+ .setFaviconOverlay(overlayDataUrl)
+ .then(() => {
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual(
+ faviconWithOverlayDataUrl,
+ );
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -512,24 +533,24 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should reset favicon in case of error', (done) => {
+ it('should reset favicon in case of error', done => {
mock.onGet(BUILD_URL).replyOnce(500);
- commonUtils.setCiStatusFavicon(BUILD_URL)
- .catch(() => {
- const favicon = document.getElementById('favicon');
+ commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => {
+ const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
- done();
- });
+ expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
+ done();
+ });
});
- it('should set page favicon to CI status favicon based on provided status', (done) => {
+ it('should set page favicon to CI status favicon based on provided status', done => {
mock.onGet(BUILD_URL).reply(200, {
favicon: overlayDataUrl,
});
- commonUtils.setCiStatusFavicon(BUILD_URL)
+ commonUtils
+ .setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
@@ -554,11 +575,15 @@ describe('common_utils', () => {
});
it('should return the svg for a linked icon', () => {
- expect(commonUtils.spriteIcon('test')).toEqual('<svg ><use xlink:href="icons.svg#test" /></svg>');
+ expect(commonUtils.spriteIcon('test')).toEqual(
+ '<svg ><use xlink:href="icons.svg#test" /></svg>',
+ );
});
it('should set svg className when passed', () => {
- expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual('<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>');
+ expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual(
+ '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>',
+ );
});
});
@@ -578,7 +603,7 @@ describe('common_utils', () => {
const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj);
- Object.keys(convertedObj).forEach((prop) => {
+ Object.keys(convertedObj).forEach(prop => {
expect(snakeRegEx.test(prop)).toBeFalsy();
expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]);
});
@@ -597,9 +622,7 @@ describe('common_utils', () => {
},
};
- expect(
- commonUtils.convertObjectPropsToCamelCase(obj),
- ).toEqual({
+ expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({
snakeKey: {
child_snake_key: 'value',
},
@@ -614,9 +637,7 @@ describe('common_utils', () => {
},
};
- expect(
- commonUtils.convertObjectPropsToCamelCase(obj, { deep: true }),
- ).toEqual({
+ expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
snakeKey: {
childSnakeKey: 'value',
},
@@ -630,9 +651,7 @@ describe('common_utils', () => {
},
];
- expect(
- commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
- ).toEqual([
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
{
childSnakeKey: 'value',
},
@@ -648,9 +667,7 @@ describe('common_utils', () => {
],
];
- expect(
- commonUtils.convertObjectPropsToCamelCase(arr, { deep: true }),
- ).toEqual([
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
[
{
childSnakeKey: 'value',
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index de6b96aab57..d699e66b8ca 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -199,11 +199,11 @@ describe('datefix', () => {
expect(datetimeUtility.pad(2)).toEqual('02');
});
- it('should not add a zero when lenght matches the default', () => {
+ it('should not add a zero when length matches the default', () => {
expect(datetimeUtility.pad(12)).toEqual('12');
});
- it('should add a 0 when lenght is smaller than the provided', () => {
+ it('should add a 0 when length is smaller than the provided', () => {
expect(datetimeUtility.pad(12, 3)).toEqual('012');
});
});
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
index a5099a2a3b8..94c6214c86a 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -1,4 +1,10 @@
-import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
+import {
+ formatRelevantDigits,
+ bytesToKiB,
+ bytesToMiB,
+ bytesToGiB,
+ numberToHumanSize,
+} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index ac3270baef5..92ebfc38722 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -120,7 +120,7 @@ describe('text_utility', () => {
});
describe('getFirstCharacterCapitalized', () => {
- it('returns the first character captialized, if first character is alphabetic', () => {
+ it('returns the first character capitalized, if first character is alphabetic', () => {
expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L');
expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S');
});
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index a837b71db0b..038bfffd44f 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import GraphFlag from '~/monitoring/components/graph/flag.vue';
import { deploymentData } from '../mock_data';
-const createComponent = (propsData) => {
+const createComponent = propsData => {
const Component = Vue.extend(GraphFlag);
return new Component({
@@ -51,8 +51,7 @@ describe('GraphFlag', () => {
it('has a line at the currentXCoordinate', () => {
component = createComponent(defaultValuesComponent);
- expect(component.$el.style.left)
- .toEqual(`${70 + component.currentXCoordinate}px`);
+ expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`);
});
describe('Deployment flag', () => {
@@ -62,9 +61,7 @@ describe('GraphFlag', () => {
deploymentFlagData,
});
- expect(
- deploymentFlagComponent.$el.querySelector('.popover-title'),
- ).toContainText('Deployed');
+ expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed');
});
it('contains the ref when a tag is available', () => {
@@ -78,13 +75,13 @@ describe('GraphFlag', () => {
},
});
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('f5bcd1d9');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ 'f5bcd1d9',
+ );
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('1.0');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ '1.0',
+ );
});
it('does not contain the ref when a tag is unavailable', () => {
@@ -98,13 +95,13 @@ describe('GraphFlag', () => {
},
});
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).toContainText('f5bcd1d9');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText(
+ 'f5bcd1d9',
+ );
- expect(
- deploymentFlagComponent.$el.querySelector('.deploy-meta-content'),
- ).not.toContainText('1.0');
+ expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText(
+ '1.0',
+ );
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 3ed70a98590..461de5a3106 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -78,7 +78,7 @@ describe('Notes Store mutations', () => {
});
describe('COLLAPSE_DISCUSSION', () => {
- it('should collpase an expanded discussion', () => {
+ it('should collapse an expanded discussion', () => {
const discussion = Object.assign({}, discussionMock, { expanded: true });
const state = {
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index e21dca45fa1..f12950b8fce 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -24,7 +24,7 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
- it('should render emtpy state information', () => {
+ it('should render empty state information', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect(
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 027066e1d4d..3d2232ff239 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -50,7 +50,7 @@ describe('pipeline graph action component', () => {
});
describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successfull request', done => {
+ it('emits `pipelineActionRequestComplete` after a successful request', done => {
spyOn(component, '$emit');
component.$el.click();
diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js
index b6fa4272c8b..96a2d5f62fa 100644
--- a/spec/javascripts/pipelines/graph/graph_component_spec.js
+++ b/spec/javascripts/pipelines/graph/graph_component_spec.js
@@ -40,7 +40,9 @@ describe('graph component', () => {
).toEqual(true);
expect(
- component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
+ component.$el
+ .querySelector('.stage-column:nth-child(2) .build:nth-child(1)')
+ .classList.contains('left-connector'),
).toEqual(true);
expect(component.$el.querySelector('loading-icon')).toBe(null);
@@ -56,7 +58,9 @@ describe('graph component', () => {
pipeline: graphJSON,
});
- expect(component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim()).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
+ expect(
+ component.$el.querySelector('.stage-column:nth-child(2) .stage-name').textContent.trim(),
+ ).toEqual('Deploy &lt;img src=x onerror=alert(document.domain)&gt;');
});
});
});
diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js
index b5c62178642..a7dcd532f4f 100644
--- a/spec/javascripts/pipelines/pipelines_actions_spec.js
+++ b/spec/javascripts/pipelines/pipelines_actions_spec.js
@@ -62,9 +62,13 @@ describe('Pipelines Actions dropdown', () => {
);
};
- beforeEach(() => {
+ beforeEach(done => {
spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
});
it('emits postAction event after confirming', () => {
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index 37908153e0e..97ded16db69 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -372,7 +372,7 @@ describe('Pipelines', () => {
});
});
- describe('successfull request', () => {
+ describe('successful request', () => {
describe('with pipelines', () => {
beforeEach(() => {
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
@@ -667,7 +667,7 @@ describe('Pipelines', () => {
});
});
- it('returns false when state is emtpy state', done => {
+ it('returns false when state is empty state', done => {
vm.isLoading = false;
vm.hasMadeRequest = true;
vm.hasGitlabCi = false;
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index a3caaeb44dc..3c8b8032de8 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -40,7 +40,7 @@ describe('Pipelines stage component', () => {
expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown');
});
- describe('with successfull request', () => {
+ describe('with successful request', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index e7f8f4f9936..eced4925489 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -78,9 +78,7 @@ describe('Assignee component', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: false,
},
}).$mount();
@@ -90,7 +88,10 @@ describe('Assignee component', () => {
expect(collapsed.childElementCount).toEqual(1);
expect(assignee.querySelector('.avatar').getAttribute('src')).toEqual(UsersMock.user.avatar);
- expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(`${UsersMock.user.name}'s avatar`);
+ expect(assignee.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${UsersMock.user.name}'s avatar`,
+ );
+
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
});
@@ -98,34 +99,38 @@ describe('Assignee component', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: true,
},
}).$mount();
expect(component.$el.querySelector('.author-link')).not.toBeNull();
// The image
- expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(UsersMock.user.avatar);
+ expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
+ UsersMock.user.avatar,
+ );
// Author name
- expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(UsersMock.user.name);
+ expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
+ UsersMock.user.name,
+ );
// Username
- expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(`@${UsersMock.user.username}`);
+ expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
+ `@${UsersMock.user.username}`,
+ );
});
it('has the root url present in the assigneeUrl method', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
- users: [
- UsersMock.user,
- ],
+ users: [UsersMock.user],
editable: true,
},
}).$mount();
- expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(-1);
+ expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
+ -1,
+ );
});
});
@@ -147,13 +152,19 @@ describe('Assignee component', () => {
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[0].name}'s avatar`,
+ );
+
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
expect(second.querySelector('.avatar').getAttribute('src')).toEqual(users[1].avatar);
- expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[1].name}'s avatar`);
+ expect(second.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[1].name}'s avatar`,
+ );
+
expect(second.querySelector('.author').innerText.trim()).toEqual(users[1].name);
});
@@ -174,7 +185,10 @@ describe('Assignee component', () => {
const first = collapsed.children[0];
expect(first.querySelector('.avatar').getAttribute('src')).toEqual(users[0].avatar);
- expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(`${users[0].name}'s avatar`);
+ expect(first.querySelector('.avatar').getAttribute('alt')).toEqual(
+ `${users[0].name}'s avatar`,
+ );
+
expect(first.querySelector('.author').innerText.trim()).toEqual(users[0].name);
const second = collapsed.children[1];
@@ -196,7 +210,7 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
- it('Shows the "show-less" assignees label', (done) => {
+ it('Shows the "show-less" assignees label', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
@@ -206,21 +220,26 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.$el.querySelectorAll('.user-item').length).toEqual(component.defaultRenderCount);
+ expect(component.$el.querySelectorAll('.user-item').length).toEqual(
+ component.defaultRenderCount,
+ );
+
expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - component.defaultRenderCount;
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .not.toBe(`+${usersLabelExpectation} more`);
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
+ `+${usersLabelExpectation} more`,
+ );
component.toggleShowLess();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
- it('Shows the "show-less" when "n+ more " label is clicked', (done) => {
+ it('Shows the "show-less" when "n+ more " label is clicked', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
@@ -232,8 +251,9 @@ describe('Assignee component', () => {
component.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
@@ -264,16 +284,18 @@ describe('Assignee component', () => {
});
it('shows "+1 more" label', () => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('+ 1 more');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '+ 1 more',
+ );
});
- it('shows "show less" label', (done) => {
+ it('shows "show less" label', done => {
component.toggleShowLess();
Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim())
- .toBe('- show less');
+ expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
+ '- show less',
+ );
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index ce850bc621e..3d44af11153 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -2,54 +2,48 @@ import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
+import mountComponent from '../../helpers/vue_mount_component_helper';
-const deploymentMockData = {
- id: 15,
- name: 'review/diplo',
- url: '/root/acets-review-apps/environments/15',
- stop_url: '/root/acets-review-apps/environments/15/stop',
- metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
- external_url: 'http://diplo.',
- external_url_formatted: 'diplo.',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes: [
- {
- path: 'index.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
- },
- ],
-};
-const createComponent = () => {
+describe('Deployment component', () => {
const Component = Vue.extend(deploymentComponent);
+ const deploymentMockData = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/review-apps/environments/15',
+ stop_url: '/root/review-apps/environments/15/stop',
+ metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
+ external_url: 'http://gitlab.com.',
+ external_url_formatted: 'gitlab',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ };
- return new Component({
- el: document.createElement('div'),
- propsData: { deployment: { ...deploymentMockData } },
- });
-};
-
-describe('Deployment component', () => {
let vm;
- beforeEach(() => {
- vm = createComponent();
- });
-
afterEach(() => {
vm.$destroy();
});
- describe('computed', () => {
+ describe('', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
+ });
+
describe('deployTimeago', () => {
it('return formatted date', () => {
const readable = getTimeago().format(deploymentMockData.deployed_at);
@@ -111,9 +105,7 @@ describe('Deployment component', () => {
expect(vm.hasDeploymentMeta).toEqual(false);
});
});
- });
- describe('methods', () => {
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () =>
@@ -152,42 +144,33 @@ describe('Deployment component', () => {
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
- });
-
- describe('template', () => {
- let el;
-
- beforeEach(() => {
- vm = createComponent(deploymentMockData);
- el = vm.$el;
- });
it('renders deployment name', () => {
- expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
+ expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
deploymentMockData.url,
);
- expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
+ expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
});
it('renders external URL', () => {
- expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
+ expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
deploymentMockData.external_url,
);
- expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
+ expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
- expect(el.querySelector('.btn')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
});
it('renders deployment time', () => {
- expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
+ expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
});
it('renders metrics component', () => {
- expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull();
});
});
@@ -196,8 +179,7 @@ describe('Deployment component', () => {
window.gon = window.gon || {};
window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = true;
-
- vm = createComponent(deploymentMockData);
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
afterEach(() => {
@@ -216,7 +198,7 @@ describe('Deployment component', () => {
window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = false;
- vm = createComponent(deploymentMockData);
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
afterEach(() => {
@@ -228,4 +210,64 @@ describe('Deployment component', () => {
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
});
});
+
+ describe('without changes', () => {
+ beforeEach(() => {
+ window.gon = window.gon || {};
+ window.gon.features = window.gon.features || {};
+ window.gon.features.ciEnvironmentsStatusChanges = true;
+ delete deploymentMockData.changes;
+
+ vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
+ });
+
+ afterEach(() => {
+ delete window.gon.features.ciEnvironmentsStatusChanges;
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
+ expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
+ });
+ });
+
+ describe('deployment status', () => {
+ describe('running', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'running' }),
+ });
+ });
+
+ it('renders information about running deployment', () => {
+ expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to');
+ });
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'success' }),
+ });
+ });
+
+ it('renders information about finished deployment', () => {
+ expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to');
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ deployment: Object.assign({}, deploymentMockData, { status: 'failed' }),
+ });
+ });
+
+ it('renders information about finished deployment', () => {
+ expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
+ 'Failed to deploy to',
+ );
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index ea8007d2029..6c7637eed13 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -22,6 +22,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
ciStatus: 'success',
hasCi: true,
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasPipeline).toEqual(true);
@@ -30,6 +31,7 @@ describe('MRWidgetPipeline', () => {
it('should return false when there is no pipeline', () => {
vm = mountComponent(Component, {
pipeline: {},
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasPipeline).toEqual(false);
@@ -42,6 +44,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasCIError).toEqual(false);
@@ -52,6 +55,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: null,
+ troubleshootingDocsPath: 'help',
});
expect(vm.hasCIError).toEqual(true);
@@ -65,11 +69,12 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: null,
+ troubleshootingDocsPath: 'help',
});
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toEqual('Could not connect to the CI server. Please check your settings and try again');
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>',
+ );
});
describe('with a pipeline', () => {
@@ -78,38 +83,41 @@ describe('MRWidgetPipeline', () => {
pipeline: mockData.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
});
it('should render pipeline ID', () => {
- expect(
- vm.$el.querySelector('.pipeline-id').textContent.trim(),
- ).toEqual(`#${mockData.pipeline.id}`);
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
});
it('should render pipeline status and commit id', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toContain(mockData.pipeline.details.status.label);
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link').textContent.trim(),
- ).toEqual(mockData.pipeline.commit.short_id);
+ expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual(
+ mockData.pipeline.commit.short_id,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link').getAttribute('href'),
- ).toEqual(mockData.pipeline.commit.commit_path);
+ expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual(
+ mockData.pipeline.commit.commit_path,
+ );
});
it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
});
it('should render coverage information', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).toContain(`Coverage ${mockData.pipeline.coverage}`);
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
});
});
@@ -122,34 +130,35 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
});
it('should render pipeline ID', () => {
- expect(
- vm.$el.querySelector('.pipeline-id').textContent.trim(),
- ).toEqual(`#${mockData.pipeline.id}`);
+ expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
+ `#${mockData.pipeline.id}`,
+ );
});
it('should render pipeline status', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent.trim(),
- ).toContain(mockData.pipeline.details.status.label);
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ mockData.pipeline.details.status.label,
+ );
- expect(
- vm.$el.querySelector('.js-commit-link'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
});
it('should render pipeline graph', () => {
expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(mockData.pipeline.details.stages.length);
+ expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
+ mockData.pipeline.details.stages.length,
+ );
});
it('should render coverage information', () => {
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).toContain(`Coverage ${mockData.pipeline.coverage}`);
+ expect(vm.$el.querySelector('.media-body').textContent).toContain(
+ `Coverage ${mockData.pipeline.coverage}`,
+ );
});
});
@@ -162,11 +171,10 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
- expect(
- vm.$el.querySelector('.media-body').textContent,
- ).not.toContain('Coverage');
+ expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage');
});
});
@@ -179,6 +187,7 @@ describe('MRWidgetPipeline', () => {
pipeline: mockCopy.pipeline,
hasCi: true,
ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
});
expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
new file mode 100644
index 00000000000..68a65bd21c6
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import component from '~/vue_merge_request_widget/components/review_app_link.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('review app link', () => {
+ const Component = Vue.extend(component);
+ const props = {
+ link: '/review',
+ cssClass: 'js-link',
+ };
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided link as href attribute', () => {
+ expect(el.getAttribute('href')).toEqual(props.link);
+ });
+
+ it('renders provided cssClass as class attribute', () => {
+ expect(el.getAttribute('class')).toEqual(props.cssClass);
+ });
+
+ it('renders View app text', () => {
+ expect(el.textContent.trim()).toEqual('View app');
+ });
+
+ it('renders svg icon', () => {
+ expect(el.querySelector('svg')).not.toBeNull();
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index d68342635ef..da5cb752c6f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -69,7 +69,7 @@ describe('MRWidgetMerged', () => {
expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
});
- it('returns false wehn sourceBranchRemoved is true', () => {
+ it('returns false when sourceBranchRemoved is true', () => {
vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 7fd1a2350f7..17554c4fe42 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -218,5 +218,7 @@ export default {
diverged_commits_count: 0,
only_allow_merge_if_pipeline_succeeds: false,
commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
- merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
+ merge_commit_path:
+ 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
+ troubleshooting_docs_path: 'help',
};
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index d1a064b9f4d..09fbe87b27e 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => {
it('should fetch deployments', done => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
- vm.fetchDeployments();
+ vm.fetchPreMergeDeployments();
setTimeout(() => {
expect(vm.service.fetchDeployments).toHaveBeenCalled();
@@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
+ status: 'success',
};
beforeEach(done => {
@@ -486,4 +487,192 @@ describe('mrWidgetOptions', () => {
).toEqual(changes.length);
});
});
+
+ describe('pipeline for target branch after merge', () => {
+ describe('with information for target branch pipeline', () => {
+ beforeEach(done => {
+ vm.mr.state = 'merged';
+ vm.mr.mergePipeline = {
+ id: 127,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ active: true,
+ coverage: null,
+ source: 'push',
+ created_at: '2018-10-22T11:41:35.186Z',
+ updated_at: '2018-10-22T11:41:35.433Z',
+ path: '/root/ci-web-terminal/pipelines/127',
+ flags: {
+ latest: true,
+ stuck: true,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'test',
+ title: 'test: pending',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ path: '/root/ci-web-terminal/pipelines/127#test',
+ dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test',
+ },
+ ],
+ artifacts: [],
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'master',
+ path: '/root/ci-web-terminal/commits/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'aa1939133d373c94879becb79d91828a892ee319',
+ short_id: 'aa193913',
+ title: "Merge branch 'master-test' into 'master'",
+ created_at: '2018-10-22T11:41:33.000Z',
+ parent_ids: [
+ '4622f4dd792468993003caf2e3be978798cbe096',
+ '76598df914cdfe87132d0c3c40f80db9fa9396a4',
+ ],
+ message:
+ "Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2018-10-22T11:41:33.000Z',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2018-10-22T11:41:33.000Z',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ author_gravatar_url: null,
+ commit_url:
+ 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ },
+ cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
+ };
+ vm.$nextTick(done);
+ });
+
+ it('renders pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull();
+ });
+
+ describe('with post merge deployments', () => {
+ beforeEach(done => {
+ vm.mr.postMergeDeployments = [
+ {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url:
+ 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ status: 'success',
+ },
+ ];
+
+ vm.$nextTick(done);
+ });
+
+ it('renders post deployment information', () => {
+ expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull();
+ });
+ });
+ });
+
+ describe('without information for target branch pipeline', () => {
+ beforeEach(done => {
+ vm.mr.state = 'merged';
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
+ });
+ });
+
+ describe('when state is not merged', () => {
+ beforeEach(done => {
+ vm.mr.state = 'archived';
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render pipeline block', () => {
+ expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
+ });
+
+ it('does not render post deployment information', () => {
+ expect(vm.$el.querySelector('.js-post-deployment')).toBeNull();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
index b71cb36ecf6..b84b5ae67a8 100644
--- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
+++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
@@ -41,7 +41,7 @@ describe('Filtered search dropdown', () => {
});
});
- describe('when visible number is bigger than the items lenght', () => {
+ describe('when visible number is bigger than the items length', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index 026a0c7ea09..c507a97d37e 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -11,29 +11,13 @@ describe('collapsedGroupedDatePicker', () => {
});
});
- it('should render toggle sidebar if showToggleSidebar', (done) => {
- expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined();
-
- vm.showToggleSidebar = false;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull();
- done();
- });
- });
-
describe('toggleCollapse events', () => {
- beforeEach((done) => {
+ beforeEach(done => {
spyOn(vm, 'toggleSidebar');
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
- it('should emit when sidebar is toggled', () => {
- vm.$el.querySelector('.gutter-toggle').click();
-
- expect(vm.toggleSidebar).toHaveBeenCalled();
- });
-
it('should emit when collapsed-calendar-icon is clicked', () => {
vm.$el.querySelector('.sidebar-collapsed-icon').click();
@@ -42,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('minDate and maxDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.minDate = new Date('07/17/2016');
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
@@ -58,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('minDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
@@ -72,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe('maxDate', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
@@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => {
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('None');
});
+
+ it('should have tooltip as `Start and due date`', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons[0].dataset.originalTitle).toBe('Start and due date');
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
index 1581f4e3eb1..805ba7b9947 100644
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
@@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
});
- it('should render date-picker when editing', (done) => {
+ it('should render date-picker when editing', done => {
vm.editing = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.pika-label')).toBeDefined();
@@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => {
});
describe('editable', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.editable = true;
Vue.nextTick(done);
});
@@ -59,7 +59,7 @@ describe('sidebarDatePicker', () => {
expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
});
- it('should enable editing when edit button is clicked', (done) => {
+ it('should enable editing when edit button is clicked', done => {
vm.isLoading = false;
Vue.nextTick(() => {
vm.$el.querySelector('.title .btn-blank').click();
@@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => {
});
});
- it('should render date if selectedDate', (done) => {
+ it('should render date if selectedDate', done => {
vm.selectedDate = new Date('07/07/2017');
Vue.nextTick(() => {
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
@@ -79,7 +79,7 @@ describe('sidebarDatePicker', () => {
});
describe('selectedDate and editable', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.selectedDate = new Date('07/07/2017');
vm.editable = true;
Vue.nextTick(done);
@@ -100,7 +100,7 @@ describe('sidebarDatePicker', () => {
});
describe('showToggleSidebar', () => {
- beforeEach((done) => {
+ beforeEach(done => {
vm.showToggleSidebar = true;
Vue.nextTick(done);
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 9a691116cf8..804b33422bd 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => {
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more');
+ expect(vmMoreLabels.labelsList).toBe(
+ 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
+ );
vmMoreLabels.$destroy();
});
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 50b8d49d4bd..e022245d3ea 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
@@ -44,7 +44,7 @@ describe('User Avatar Link Component', function() {
expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
});
- it('should return neccessary props as defined', function() {
+ it('should return necessary props as defined', function() {
_.each(this.propsData, (val, key) => {
expect(this.userAvatarLink[key]).toBeDefined();
});
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index a50329473ad..7a457403b51 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -76,7 +76,7 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a')['href']).to eq link
end
- it 'autolinks multiple occurences of smb' do
+ it 'autolinks multiple occurrences of smb' do
link1 = 'smb:///Volumes/shared/foo.pdf'
link2 = 'smb:///Volumes/shared/bar.pdf'
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index ed1ebe9ebf6..415ded05e6e 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -226,7 +226,7 @@ describe Banzai::Filter::RelativeLinkFilter do
let(:ref) {'mark#\'@],+;-._/#@!$&()+down'}
it 'correctly escapes the ref' do
- # Adressable won't escape the '#', so we do this manually
+ # Addressable won't escape the '#', so we do this manually
ref_escaped = 'mark%23\'@%5D,+;-._/%23@!$&()+down'
# Stub this method so the branch doesn't actually need to be in the repo
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index c73faa55513..d3fff5bad42 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -64,7 +64,7 @@ describe ContainerRegistry::Blob do
.to_return(status: 200)
end
- it 'returns true when blob has been successfuly deleted' do
+ it 'returns true when blob has been successfully deleted' do
expect(blob.delete).to be_truthy
end
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index e1c4f9cfea7..5076996474f 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -118,7 +118,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
expect(fork_network_members.count).to eq(12)
end
- it 'knows when not all memberships withing a batch have been created' do
+ it 'knows when not all memberships within a batch have been created' do
expect(migration.missing_members?(8, 10)).to be_truthy
end
end
diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb
new file mode 100644
index 00000000000..3e107ac3027
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'token is not yet hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ end
+
+ it 'saves token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01')))
+ end
+
+ it 'erases token' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
+ change { PersonalAccessToken.find(1).token }.from('token-01').to(nil))
+ end
+ end
+
+ context 'token is already hashed' do
+ before do
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01')
+ end
+
+ it 'does not change existing token digest' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token_digest })
+ end
+
+ it 'leaves token empty' do
+ expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
+ change { PersonalAccessToken.find(1).token }.from(nil))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/redact_links_spec.rb b/spec/lib/gitlab/background_migration/redact_links_spec.rb
new file mode 100644
index 00000000000..a40e68069cc
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/redact_links_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::RedactLinks, :migration, schema: 20181014121030 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:notes) { table(:notes) }
+ let(:snippets) { table(:snippets) }
+ let(:users) { table(:users) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+
+ def create_merge_request(id, params)
+ params.merge!(id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+
+ merge_requests.create(params)
+ end
+
+ def create_issue(id, params)
+ params.merge!(id: id, title: "issue#{id}", project_id: project.id)
+
+ issues.create(params)
+ end
+
+ def create_note(id, params)
+ params[:id] = id
+
+ notes.create(params)
+ end
+
+ def create_snippet(id, params)
+ params.merge!(id: id, author_id: user.id)
+
+ snippets.create(params)
+ end
+
+ def create_resource(model, id, params)
+ send("create_#{model.name.underscore}", id, params)
+ end
+
+ shared_examples_for 'redactable resource' do
+ it 'updates only matching texts' do
+ matching_text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+ redacted_text = 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ create_resource(model, 1, { field => matching_text })
+ create_resource(model, 2, { field => 'not matching text' })
+ create_resource(model, 3, { field => matching_text })
+ create_resource(model, 4, { field => redacted_text })
+ create_resource(model, 5, { field => matching_text })
+
+ expected = { field => 'some text /sent_notifications/REDACTED/unsubscribe more text',
+ "#{field}_html" => nil }
+ expect_any_instance_of("Gitlab::BackgroundMigration::RedactLinks::#{model}".constantize).to receive(:update_columns).with(expected).and_call_original
+
+ subject.perform(model, field, 2, 4)
+
+ expect(model.where(field => matching_text).pluck(:id)).to eq [1, 5]
+ expect(model.find(3).reload[field]).to eq redacted_text
+ end
+ end
+
+ context 'resource is Issue' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Issue }
+ let(:field) { :description }
+ end
+ end
+
+ context 'resource is Merge Request' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { MergeRequest }
+ let(:field) { :description }
+ end
+ end
+
+ context 'resource is Note' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Note }
+ let(:field) { :note }
+ end
+ end
+
+ context 'resource is Snippet' do
+ it_behaves_like 'redactable resource' do
+ let(:model) { Snippet }
+ let(:field) { :description }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index 4d5081b0a75..e5999a1c509 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
expect(pipeline_status.status).to eq(status)
expect(pipeline_status.ref).to eq(ref)
end
+
+ context 'when status is empty string' do
+ before do
+ Gitlab::Redis::Cache.with do |redis|
+ redis.mapped_hmset(cache_key,
+ { sha: sha, status: '', ref: ref })
+ end
+ end
+
+ it 'reads the status as nil' do
+ pipeline_status.load_from_cache
+
+ expect(pipeline_status.status).to eq(nil)
+ end
+ end
end
describe '#has_cache?' do
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 4df426c54ae..81804ba5c76 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do
let(:ref) { 'refs/heads/master' }
let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
+ let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
+ let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
subject(:change_access) do
described_class.new(
changes,
project: project,
user_access: user_access,
- protocol: protocol
+ protocol: protocol,
+ logger: logger
)
end
@@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do
end
end
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
+
+ expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+ end
+
context 'when the user is not allowed to push to the repo' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index ec22e3a198e..887ea8fc1e0 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::LfsIntegrity do
include ProjectForksHelper
+ let!(:time_left) { 50 }
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
@@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do
operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
end
- subject { described_class.new(project, newrev) }
+ subject { described_class.new(project, newrev, time_left) }
describe '#objects_missing?' do
let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
@@ -67,7 +68,7 @@ describe Gitlab::Checks::LfsIntegrity do
expect(subject.objects_missing?).to be_truthy
end
- it 'is false parent project already conatins LFS objects for the fork' do
+ it 'is false parent project already contains LFS objects for the fork' do
lfs_object = create(:lfs_object, oid: blob_object.lfs_oid)
create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object)
diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb
new file mode 100644
index 00000000000..0ed3940c038
--- /dev/null
+++ b/spec/lib/gitlab/checks/timed_logger_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Checks::TimedLogger do
+ let!(:timeout) { 50.seconds }
+ let!(:start) { Time.now }
+ let!(:ref) { "bar" }
+ let!(:logger) { described_class.new(start_time: start, timeout: timeout) }
+ let!(:log_messages) do
+ {
+ foo: "Foo message..."
+ }
+ end
+
+ before do
+ logger.append_message("Checking ref: #{ref}")
+ end
+
+ describe '#log_timed' do
+ it 'logs message' do
+ Timecop.freeze(start + 30.seconds) do
+ logger.log_timed(log_messages[:foo], start) { bar_check }
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)")
+ end
+
+ context 'when time limit was reached' do
+ it 'cancels action' do
+ Timecop.freeze(start + 50.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ bar_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+ end
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)")
+ end
+
+ it 'cancels action with time elapsed if work was performed' do
+ Timecop.freeze(start + 30.seconds) do
+ expect do
+ logger.log_timed(log_messages[:foo], start) do
+ grpc_check
+ end
+ end.to raise_error(described_class::TimeoutError)
+
+ expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)")
+ end
+ end
+ end
+ end
+
+ def bar_check
+ 2 + 2
+ end
+
+ def grpc_check
+ raise GRPC::DeadlineExceeded
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index 7549e9941b6..5a5c071c639 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Ci::Ansi2html do
expect(convert_html("Hello")).to eq('Hello')
end
- it "strips non-color-changing controll sequences" do
+ it "strips non-color-changing control sequences" do
expect(convert_html("Hello \e[2Kworld")).to eq('Hello world')
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 2ce858836e3..c2c0742efc3 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).to be_satisfied_by(pipeline, seed)
end
- it 'is not satisfied by an overriden empty variable' do
+ it 'is not satisfied by an overridden empty variable' do
policy = described_class.new(['$CI_PROJECT_NAME'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
@@ -54,7 +54,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
- it 'allows to evaluate regular secret variables' do
+ it 'allows to evaluate regular CI variables' do
create(:ci_variable, project: project, key: 'SECRET', value: 'my secret')
policy = described_class.new(["$SECRET == 'my secret'"])
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 1860ed79bfd..7c18514934e 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -219,7 +219,7 @@ describe Gitlab::Ci::Config::Entry::Global do
##
# When nodes are specified but not defined, we assume that
- # configuration is valid, and we asume that entry is simply undefined,
+ # configuration is valid, and we assume that entry is simply undefined,
# despite the fact, that key is present. See issue #18775 for more
# details.
#
diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
index bc9dbf2bece..239fa364f5e 100644
--- a/spec/lib/gitlab/cross_project_access/check_info_spec.rb
+++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb
@@ -50,7 +50,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do
expect(info.should_run?(dummy_controller)).to be_truthy
end
- it 'returns the the oposite of #should_skip? when the check is a skip' do
+ it 'returns the the opposite of #should_skip? when the check is a skip' do
info = described_class.new({}, nil, nil, true)
expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false)
@@ -101,7 +101,7 @@ describe Gitlab::CrossProjectAccess::CheckInfo do
expect(info.should_skip?(dummy_controller)).to be_truthy
end
- it 'returns the the oposite of #should_run? when the check is not a skip' do
+ it 'returns the the opposite of #should_run? when the check is not a skip' do
info = described_class.new({}, nil, nil, false)
expect(info).to receive(:should_run?).with(dummy_controller).and_return(false)
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index cc7cb3f23fd..248cca25a2c 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete
end
describe "#remove_last_ocurrence" do
- it "removes only the last occurance of a string" do
+ it "removes only the last occurrence of a string" do
input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 2d94356f386..cc4faf6f10b 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -566,13 +566,13 @@ describe Gitlab::Diff::Position do
end
end
- context "for text positon" do
+ context "for text position" do
let(:args) { args_for_text }
it_behaves_like "diff position json"
end
- context "for image positon" do
+ context "for image position" do
let(:args) { args_for_img }
it_behaves_like "diff position json"
@@ -592,13 +592,13 @@ describe Gitlab::Diff::Position do
end
end
- context "for text positon" do
+ context "for text position" do
let(:args) { args_for_text }
it_behaves_like "diff position json"
end
- context "for image positon" do
+ context "for image position" do
let(:args) { args_for_img }
it_behaves_like "diff position json"
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index ddc4f6c5b5c..a2eed07ca55 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -43,7 +43,7 @@ describe Gitlab::Diff::PositionTracer do
#
# In any case, all of this means that the tests below will be extremely
# (excessively, unjustifiably) thorough for scenarios where "the file was
- # created in the old diff" and then drop off to comparitively lackluster
+ # created in the old diff" and then drop off to comparatively lackluster
# testing of other scenarios.
#
# I did still try to cover most of the obvious and potentially tricky
diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
index ca067a29174..134bd5657e7 100644
--- a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Git::AttributesAtRefParser, :seed_helper do
end
it 'handles missing blobs' do
- expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error
+ expect { described_class.new(repository, 'non-existent-branch') }.not_to raise_error
end
describe '#attributes' do
diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index 18ebfef38f0..f431d4e2a53 100644
--- a/spec/lib/gitlab/git/attributes_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -94,7 +94,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
# It's a bit hard to test for something _not_ being processed. As such we'll
# just test the number of entries.
it 'ignores any comments and empty lines' do
- expect(subject.patterns.length).to eq(10)
+ expect(subject.patterns.length).to eq(12)
end
end
@@ -126,7 +126,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
describe '#each_line' do
it 'iterates over every line in the attributes file' do
- args = [String] * 14 # the number of lines in the file
+ args = [String] * 16 # the number of lines in the file
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
index c5e7ab959b2..d035df7e0c2 100644
--- a/spec/lib/gitlab/git/lfs_changes_spec.rb
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do
it 'limits new_objects using object_limit' do
expect(subject.new_pointers(object_limit: 1)).to eq([])
end
+
+ it 'times out if given a small dynamic timeout' do
+ expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded)
+ end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 51eb997a325..9a443fa7f20 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1194,6 +1194,34 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#gitattribute' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') }
+
+ after do
+ ensure_seeds
+ end
+
+ it 'returns matching language attribute' do
+ expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby')
+ end
+
+ it 'returns matching language attribute with additional options' do
+ expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json')
+ end
+
+ it 'returns nil if nothing matches' do
+ expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil)
+ end
+
+ context 'without gitattributes file' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+
+ it 'returns nil' do
+ expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil)
+ end
+ end
+ end
+
describe '#ref_exists?' do
it 'returns true for an existing tag' do
expect(repository.ref_exists?('refs/heads/master')).to eq(true)
diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
new file mode 100644
index 00000000000..bcf4814edb6
--- /dev/null
+++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::Git::WrapsGitalyErrors do
+ subject(:wrapper) do
+ klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors }
+ klazz.new
+ end
+
+ describe "#wrapped_gitaly_errors" do
+ mapping = {
+ GRPC::NotFound => Gitlab::Git::Repository::NoRepository,
+ GRPC::InvalidArgument => ArgumentError,
+ GRPC::BadStatus => Gitlab::Git::CommandError
+ }
+
+ mapping.each do |grpc_error, error|
+ it "wraps #{grpc_error} in a #{error}" do
+ expect { wrapper.wrapped_gitaly_errors { raise grpc_error.new('wrapped') } }
+ .to raise_error(error)
+ end
+ end
+
+ it 'does not swallow other errors' do
+ expect { wrapper.wrapped_gitaly_errors { raise 'raised' } }
+ .to raise_error(RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index e7da5565c26..a417ef77c9e 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -934,6 +934,16 @@ describe Gitlab::GitAccess do
# There is still an N+1 query with protected branches
expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1)
end
+
+ it 'raises TimeoutError when #check_single_change_access raises a timeout error' do
+ message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow"
+
+ expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check|
+ expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+
+ expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message)
+ end
end
end
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 47f37cae98f..39d09c49989 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -96,7 +96,7 @@ describe Gitlab::Gpg do
expect(described_class.current_home_dir).to eq default_home_dir
end
- it 'returns the explicitely set home dir' do
+ it 'returns the explicitly set home dir' do
GPGME::Engine.home_dir = '/tmp/gpg'
expect(described_class.current_home_dir).to eq '/tmp/gpg'
@@ -104,7 +104,7 @@ describe Gitlab::Gpg do
GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir')
end
- it 'returns the default value when explicitely setting the home dir to nil' do
+ it 'returns the default value when explicitly setting the home dir to nil' do
GPGME::Engine.home_dir = nil
expect(described_class.current_home_dir).to eq default_home_dir
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 88f7099ff3c..fe0e9702f8a 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -5,44 +5,85 @@ describe Gitlab::Highlight do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:commit) { project.commit(sample_commit.id) }
-
- describe 'custom highlighting from .gitattributes' do
- let(:branch) { 'gitattributes' }
- let(:blob) { repository.blob_at_branch(branch, path) }
+ describe 'language provided' do
let(:highlighter) do
- described_class.new(blob.path, blob.data, repository: repository)
+ described_class.new('foo.erb', 'bar', language: 'erb?parent=json')
end
- before do
- project.change_head('gitattributes')
+ it 'sets correct lexer' do
+ expect(highlighter.lexer.tag).to eq 'erb'
+ expect(highlighter.lexer.parent.tag).to eq 'json'
end
+ end
- describe 'basic language selection' do
- let(:path) { 'custom-highlighting/test.gitlab-custom' }
- it 'highlights as ruby' do
- expect(highlighter.lexer.tag).to eq 'ruby'
- end
+ describe '#highlight' do
+ let(:file_name) { 'test.lisp' }
+ let(:no_context_content) { ":type \"assem\"))" }
+ let(:content) { "(make-pathname :defaults name\n#{no_context_content}" }
+ let(:multiline_content) do
+ %q(
+ def test(input):
+ """This is line 1 of a multi-line comment.
+ This is line 2.
+ """
+ )
+ end
+
+ it 'highlights' do
+ expected = %Q[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>]
+
+ expect(described_class.highlight(file_name, content)).to eq(expected)
+ end
+
+ it 'returns plain version for unknown lexer context' do
+ result = described_class.highlight(file_name, no_context_content)
+
+ expect(result).to eq(%[<span id="LC1" class="line" lang="">:type "assem"))</span>])
end
- describe 'cgi options' do
- let(:path) { 'custom-highlighting/test.gitlab-cgi' }
+ it 'returns plain version for long content' do
+ stub_const('Gitlab::Highlight::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1)
+ result = described_class.highlight(file_name, content)
- it 'highlights as json with erb' do
- expect(highlighter.lexer.tag).to eq 'erb'
- expect(highlighter.lexer.parent.tag).to eq 'json'
+ expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span>])
+ end
+
+ it 'highlights multi-line comments' do
+ result = described_class.highlight(file_name, multiline_content)
+ html = Nokogiri::HTML(result)
+ lines = html.search('.s')
+
+ expect(lines.count).to eq(3)
+ expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.')
+ expect(lines[1].text).to eq(' This is line 2.')
+ expect(lines[2].text).to eq(' """')
+ end
+
+ context 'diff highlighting' do
+ let(:file_name) { 'test.diff' }
+ let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
+ let(:expected) do
+ %q(<span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC4" class="line" lang="diff"> ddd</span>)
+ end
+
+ it 'highlights each line properly' do
+ result = described_class.highlight(file_name, content)
+
+ expect(result).to eq(expected)
end
end
- end
- describe '#highlight' do
describe 'with CRLF' do
let(:branch) { 'crlf-diff' }
let(:path) { 'files/whitespace' }
let(:blob) { repository.blob_at_branch(branch, path) }
let(:lines) do
- described_class.highlight(blob.path, blob.data, repository: repository).lines
+ described_class.highlight(blob.path, blob.data).lines
end
it 'strips extra LFs' do
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index 0385dd762c2..1e583f4cee2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -11,11 +11,8 @@ describe Gitlab::Identifier do
describe '#identify' do
context 'without an identifier' do
- it 'identifies the user using a commit' do
- expect(identifier).to receive(:identify_using_commit)
- .with(project, '123')
-
- identifier.identify('', project, '123')
+ it 'returns nil' do
+ expect(identifier.identify('')).to be nil
end
end
@@ -24,7 +21,7 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_user)
.with("user-#{user.id}")
- identifier.identify("user-#{user.id}", project, '123')
+ identifier.identify("user-#{user.id}")
end
end
@@ -33,49 +30,11 @@ describe Gitlab::Identifier do
expect(identifier).to receive(:identify_using_ssh_key)
.with("key-#{key.id}")
- identifier.identify("key-#{key.id}", project, '123')
+ identifier.identify("key-#{key.id}")
end
end
end
- describe '#identify_using_commit' do
- it "returns the User for an existing commit author's Email address" do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
-
- it 'returns nil when no user could be found' do
- allow(project).to receive(:commit).with('123').and_return(nil)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'returns nil when the commit does not have an author Email' do
- commit = double(:commit, author_email: nil)
-
- expect(project).to receive(:commit).with('123').and_return(commit)
-
- expect(identifier.identify_using_commit(project, '123')).to be_nil
- end
-
- it 'caches the found users per Email' do
- commit = double(:commit, author: user, author_email: user.email)
-
- expect(project).to receive(:commit).with('123').twice.and_return(commit)
-
- 2.times do
- expect(identifier.identify_using_commit(project, '123')).to eq(user)
- end
- end
-
- it 'returns nil if the project & ref are not present' do
- expect(identifier.identify_using_commit(nil, nil)).to be_nil
- end
- end
-
describe '#identify_using_user' do
it 'returns the User for an existing ID in the identifier' do
found = identifier.identify_using_user("user-#{user.id}")
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index b0570680d5a..365bfae0d88 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -321,7 +321,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- context 'when the project has overriden params in import data' do
+ context 'when the project has overridden params in import data' do
it 'overwrites the params stored in the JSON' do
project.create_import_data(data: { override_params: { description: "Overridden" } })
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index b333b334f36..c92bc92c42d 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should generate the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('alpine:3.6')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.7.2-kube-1.11.0')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
index da3f5d27b25..a1a59533bfb 100644
--- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
let(:role_ref) do
{
apiGroup: 'rbac.authorization.k8s.io',
- kind: 'Role',
+ kind: 'ClusterRole',
name: role_name
}
end
@@ -35,6 +35,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
subject do
described_class.new(
+ name: "gitlab-#{namespace}",
role_name: role_name,
namespace: namespace,
service_account_name: service_account_name
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 624e2add860..8df0facdab3 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
+ it 'returns true for localhost IPs' do
+ expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true
+ end
+
+ it 'returns true for loopback IP' do
+ expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true
+ end
+
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true
end
@@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do
end
end
+ it 'allows localhost endpoints' do
+ expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true)
+ expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true)
+ end
+
+ it 'allows loopback endpoints' do
+ expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true)
+ end
+
it 'allows IPv4 link-local endpoints' do
expect(described_class).not_to be_blocked_url('http://169.254.169.254')
expect(described_class).not_to be_blocked_url('http://169.254.168.100')
@@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)])
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)])
end
def unstub_domain_resolv
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 4eca53032a2..02c2fd47197 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -40,7 +40,7 @@ describe Gitlab::View::Presenter::Base do
end
end
- context 'subject is overriden' do
+ context 'subject is overridden' do
it 'returns true' do
presenter = presenter_class.new(build_stubbed(:project, :public))
diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb
index c9756544bd6..2aaa7c24ad8 100644
--- a/spec/lib/microsoft_teams/notifier_spec.rb
+++ b/spec/lib/microsoft_teams/notifier_spec.rb
@@ -48,7 +48,7 @@ describe MicrosoftTeams::Notifier do
stub_request(:post, webhook_url).with(body: JSON(body), headers: { 'Content-Type' => 'application/json' }).to_return(status: 200, body: "", headers: {})
end
- it 'expects to receive successfull answer' do
+ it 'expects to receive successful answer' do
expect(subject.ping(options)).to be true
end
end
diff --git a/spec/migrations/enqueue_redact_links_spec.rb b/spec/migrations/enqueue_redact_links_spec.rb
new file mode 100644
index 00000000000..a5da76977b7
--- /dev/null
+++ b/spec/migrations/enqueue_redact_links_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181014121030_enqueue_redact_links.rb')
+
+describe EnqueueRedactLinks, :migration, :sidekiq do
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:notes) { table(:notes) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:snippets) { table(:snippets) }
+ let(:users) { table(:users) }
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+
+ text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+ group = namespaces.create!(name: 'gitlab', path: 'gitlab')
+ project = projects.create!(namespace_id: group.id)
+
+ merge_requests.create!(id: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master', description: text)
+ issues.create!(id: 1, description: text)
+ notes.create!(id: 1, note: text)
+ notes.create!(id: 2, note: text)
+ snippets.create!(id: 1, description: text, author_id: user.id)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Note", "note", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, "Note", "note", 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Issue", "description", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "MergeRequest", "description", 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Snippet", "description", 1, 1)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 5
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index 4187ab149a5..af77d64fdbf 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -76,7 +76,7 @@ describe MigrateOldArtifacts do
end
end
- context 'when there are aritfacts in old and new directory' do
+ context 'when there are artifacts in old and new directory' do
before do
store_artifacts_in_legacy_path(build2)
diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..6d155f78342
--- /dev/null
+++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb')
+
+describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do
+ let(:personal_access_tokens) { table(:personal_access_tokens) }
+ let(:users) { table(:users) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 4)
+
+ users.create(id: 1, email: 'user@example.com', projects_limit: 10)
+
+ personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01')
+ personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02')
+ personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest')
+ personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04')
+ personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05')
+ personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5))
+ expect(described_class::MIGRATION).to(
+ be_scheduled_delayed_migration(
+ 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6))
+ expect(BackgroundMigrationWorker.jobs.size).to eq 2
+ end
+ end
+
+ it 'schedules background migrations' do
+ perform_enqueued_jobs do
+ plain_text_token = 'token IS NOT NULL'
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 5
+
+ migrate!
+
+ expect(personal_access_tokens.where(plain_text_token).count).to eq 0
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a046541031e..13a4aaa8936 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -216,14 +216,6 @@ describe Ci::Build do
let(:build) { create(:ci_build, :created, :schedulable, project: project) }
it { expect(subject).to be_truthy }
-
- context 'when feature flag is diabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: false)
- end
-
- it { expect(subject).to be_falsy }
- end
end
context 'when build is not schedulable' do
@@ -327,10 +319,6 @@ describe Ci::Build do
describe '#enqueue_scheduled' do
subject { build.enqueue_scheduled }
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
context 'when build is scheduled and the right time has not come yet' do
let(:build) { create(:ci_build, :scheduled, pipeline: pipeline) }
@@ -2027,17 +2015,17 @@ describe Ci::Build do
it { is_expected.to include(tag_variable) }
end
- context 'when secret variable is defined' do
- let(:secret_variable) do
+ context 'when CI variable is defined' do
+ let(:ci_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
create(:ci_variable,
- secret_variable.slice(:key, :value).merge(project: project))
+ ci_variable.slice(:key, :value).merge(project: project))
end
- it { is_expected.to include(secret_variable) }
+ it { is_expected.to include(ci_variable) }
end
context 'when protected variable is defined' do
@@ -2072,17 +2060,17 @@ describe Ci::Build do
end
end
- context 'when group secret variable is defined' do
- let(:secret_variable) do
+ context 'when group CI variable is defined' do
+ let(:ci_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
create(:ci_group_variable,
- secret_variable.slice(:key, :value).merge(group: group))
+ ci_variable.slice(:key, :value).merge(group: group))
end
- it { is_expected.to include(secret_variable) }
+ it { is_expected.to include(ci_variable) }
end
context 'when group protected variable is defined' do
@@ -2357,7 +2345,7 @@ describe Ci::Build do
.to receive(:predefined_variables) { [project_pre_var] }
allow_any_instance_of(Project)
- .to receive(:secret_variables_for)
+ .to receive(:ci_variables_for)
.with(ref: 'master', environment: nil) do
[create(:ci_variable, key: 'secret', value: 'value')]
end
@@ -2508,7 +2496,7 @@ describe Ci::Build do
end
describe '#scoped_variables_hash' do
- context 'when overriding secret variables' do
+ context 'when overriding CI variables' do
before do
project.variables.create!(key: 'MY_VAR', value: 'my value 1')
pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index f9776acd4c8..48ba163b38c 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -109,14 +109,20 @@ describe Clusters::Applications::Prometheus do
end
context 'cluster has kubeclient' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url }
let(:kube_client) { subject.cluster.kubeclient.core_client }
- subject { create(:clusters_applications_prometheus) }
+ subject { create(:clusters_applications_prometheus, cluster: cluster) }
before do
subject.cluster.platform_kubernetes.namespace = 'a-namespace'
- stub_kubeclient_discover(subject.cluster.platform_kubernetes.api_url)
+ stub_kubeclient_discover(cluster.platform_kubernetes.api_url)
+
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
end
it 'creates proxy prometheus rest client' do
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index f5c4b0b66ae..19b76ca8cfb 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -4,7 +4,10 @@ require 'spec_helper'
describe Clusters::Cluster do
it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
+ it { is_expected.to have_many(:cluster_groups) }
+ it { is_expected.to have_many(:groups) }
it { is_expected.to have_one(:provider_gcp) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:application_helm) }
@@ -13,6 +16,7 @@ describe Clusters::Cluster do
it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
it { is_expected.to have_one(:kubernetes_namespace) }
+ it { is_expected.to have_one(:cluster_project) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
@@ -178,6 +182,53 @@ describe Clusters::Cluster do
it { expect(cluster.update(enabled: false)).to be_truthy }
end
end
+
+ describe 'cluster_type validations' do
+ let(:instance_cluster) { create(:cluster, :instance) }
+ let(:group_cluster) { create(:cluster, :group) }
+ let(:project_cluster) { create(:cluster, :project) }
+
+ it 'validates presence' do
+ cluster = build(:cluster, :project, cluster_type: nil)
+
+ expect(cluster).not_to be_valid
+ expect(cluster.errors.full_messages).to include("Cluster type can't be blank")
+ end
+
+ context 'project_type cluster' do
+ it 'does not allow setting group' do
+ project_cluster.groups << build(:group)
+
+ expect(project_cluster).not_to be_valid
+ expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
+ end
+ end
+
+ context 'group_type cluster' do
+ it 'does not allow setting project' do
+ group_cluster.projects << build(:project)
+
+ expect(group_cluster).not_to be_valid
+ expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
+ end
+ end
+
+ context 'instance_type cluster' do
+ it 'does not allow setting group' do
+ instance_cluster.groups << build(:group)
+
+ expect(instance_cluster).not_to be_valid
+ expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
+ end
+
+ it 'does not allow setting project' do
+ instance_cluster.projects << build(:project)
+
+ expect(instance_cluster).not_to be_valid
+ expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
+ end
+ end
+ end
end
describe '#provider' do
@@ -229,6 +280,23 @@ describe Clusters::Cluster do
end
end
+ describe '#group' do
+ subject { cluster.group }
+
+ context 'when cluster belongs to a group' do
+ let(:cluster) { create(:cluster, :group) }
+ let(:group) { cluster.groups.first }
+
+ it { is_expected.to eq(group) }
+ end
+
+ context 'when cluster does not belong to any group' do
+ let(:cluster) { create(:cluster) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#applications' do
set(:cluster) { create(:cluster) }
diff --git a/spec/models/clusters/group_spec.rb b/spec/models/clusters/group_spec.rb
new file mode 100644
index 00000000000..ba145342cb8
--- /dev/null
+++ b/spec/models/clusters/group_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Group do
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to belong_to(:group) }
+end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index dea58fa26c7..0dfeea5cd2f 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -10,23 +10,15 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
describe 'namespace uniqueness validation' do
let(:cluster_project) { create(:cluster_project) }
-
- let(:kubernetes_namespace) do
- build(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
- end
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') }
subject { kubernetes_namespace }
context 'when cluster is using the namespace' do
before do
create(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project,
- namespace: kubernetes_namespace.namespace)
+ cluster: kubernetes_namespace.cluster,
+ namespace: 'my-namespace')
end
it { is_expected.not_to be_valid }
@@ -37,48 +29,79 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
end
end
- describe '#set_namespace_and_service_account_to_default' do
- let(:cluster) { platform.cluster }
- let(:cluster_project) { create(:cluster_project, cluster: cluster) }
- let(:kubernetes_namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster_project.cluster,
- project: cluster_project.project,
- cluster_project: cluster_project)
- end
+ describe '#configure_predefined_variables' do
+ let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace) }
+ let(:cluster) { kubernetes_namespace.cluster }
+ let(:platform) { kubernetes_namespace.platform_kubernetes }
- describe 'namespace' do
- let(:platform) { create(:cluster_platform_kubernetes, namespace: namespace) }
+ subject { kubernetes_namespace.configure_predefined_credentials }
- subject { kubernetes_namespace.namespace }
+ describe 'namespace' do
+ before do
+ platform.update_column(:namespace, namespace)
+ end
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
it 'should copy the namespace' do
- is_expected.to eq('platform-namespace')
+ subject
+
+ expect(kubernetes_namespace.namespace).to eq('platform-namespace')
end
end
context 'when platform does not have namespace assigned' do
+ let(:project) { kubernetes_namespace.project }
let(:namespace) { nil }
+ let(:project_slug) { "#{project.path}-#{project.id}" }
- it 'should set default namespace' do
- project_slug = "#{cluster_project.project.path}-#{cluster_project.project_id}"
+ it 'should fallback to project namespace' do
+ subject
- is_expected.to eq(project_slug)
+ expect(kubernetes_namespace.namespace).to eq(project_slug)
end
end
end
describe 'service_account_name' do
- let(:platform) { create(:cluster_platform_kubernetes) }
-
- subject { kubernetes_namespace.service_account_name }
+ let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" }
it 'should set a service account name based on namespace' do
- is_expected.to eq("#{kubernetes_namespace.namespace}-service-account")
+ subject
+
+ expect(kubernetes_namespace.service_account_name).to eq(service_account_name)
end
end
end
+
+ describe '#predefined_variables' do
+ let(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, service_account_token: token) }
+ let(:cluster) { create(:cluster, :project, platform_kubernetes: platform) }
+ let(:platform) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+
+ let(:api_url) { 'https://kube.domain.com' }
+ let(:ca_pem) { 'CA PEM DATA' }
+ let(:token) { 'token' }
+
+ let(:kubeconfig) do
+ config_file = expand_fixture_path('config/kubeconfig.yml')
+ config = YAML.safe_load(File.read(config_file))
+ config.dig('users', 0, 'user')['token'] = token
+ config.dig('contexts', 0, 'context')['namespace'] = kubernetes_namespace.namespace
+ config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
+ Base64.strict_encode64(ca_pem)
+
+ YAML.dump(config)
+ end
+
+ it 'sets the variables' do
+ expect(kubernetes_namespace.predefined_variables).to include(
+ { key: 'KUBE_SERVICE_ACCOUNT', value: kubernetes_namespace.service_account_name, public: true },
+ { key: 'KUBE_NAMESPACE', value: kubernetes_namespace.namespace, public: true },
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }
+ )
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index e13eb554add..2bcccc8184a 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -124,9 +124,17 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
describe '#kubeclient' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace', cluster: cluster) }
+
subject { kubernetes.kubeclient }
- let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace') }
+ before do
+ create(:cluster_kubernetes_namespace,
+ cluster: kubernetes.cluster,
+ cluster_project: kubernetes.cluster.cluster_project,
+ project: kubernetes.cluster.cluster_project.project)
+ end
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) }
end
@@ -186,29 +194,14 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
describe '#predefined_variables' do
let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
- let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) }
let(:api_url) { 'https://kube.domain.com' }
let(:ca_pem) { 'CA PEM DATA' }
- let(:token) { 'token' }
-
- let(:kubeconfig) do
- config_file = expand_fixture_path('config/kubeconfig.yml')
- config = YAML.load(File.read(config_file))
- config.dig('users', 0, 'user')['token'] = token
- config.dig('contexts', 0, 'context')['namespace'] = namespace
- config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
- Base64.strict_encode64(ca_pem)
-
- YAML.dump(config)
- end
shared_examples 'setting variables' do
it 'sets the variables' do
- expect(kubernetes.predefined_variables).to include(
+ expect(kubernetes.predefined_variables(project: cluster.project)).to include(
{ key: 'KUBE_URL', value: api_url, public: true },
- { key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: namespace, public: true },
- { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
{ key: 'KUBE_CA_PEM', value: ca_pem, public: true },
{ key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
)
@@ -229,13 +222,6 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
let(:namespace) { kubernetes.actual_namespace }
it_behaves_like 'setting variables'
-
- it 'sets the KUBE_NAMESPACE' do
- kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
-
- expect(kube_namespace).not_to be_nil
- expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
- end
end
end
@@ -319,4 +305,27 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to include(pods: []) }
end
end
+
+ describe '#update_kubernetes_namespace' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:platform) { cluster.platform }
+
+ context 'when namespace is updated' do
+ it 'should call ConfigureWorker' do
+ expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id).once
+
+ platform.namespace = 'new-namespace'
+ platform.save
+ end
+ end
+
+ context 'when namespace is not updated' do
+ it 'should not call ConfigureWorker' do
+ expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async)
+
+ platform.username = "new-username"
+ platform.save
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 69083bdc125..debc02fa51f 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -24,13 +24,29 @@ describe Awardable do
end
end
- describe ".awarded" do
+ describe "#awarded" do
it "filters by user and emoji name" do
expect(Issue.awarded(award_emoji.user, "thumbsup")).to be_empty
expect(Issue.awarded(award_emoji.user, "thumbsdown")).to eq [issue]
expect(Issue.awarded(award_emoji2.user, "thumbsup")).to eq [issue2]
expect(Issue.awarded(award_emoji2.user, "thumbsdown")).to be_empty
end
+
+ it "filters by user and any emoji" do
+ issue3 = create(:issue)
+ 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 eq [issue, issue3]
+ expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3]
+ end
+ end
+
+ describe "#not_awarded" do
+ it "returns issues not awarded by user" do
+ expect(Issue.not_awarded(award_emoji.user)).to eq [issue2]
+ expect(Issue.not_awarded(award_emoji2.user)).to eq [issue]
+ end
end
end
diff --git a/spec/models/concerns/blob_language_from_git_attributes_spec.rb b/spec/models/concerns/blob_language_from_git_attributes_spec.rb
new file mode 100644
index 00000000000..7f05073b08e
--- /dev/null
+++ b/spec/models/concerns/blob_language_from_git_attributes_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BlobLanguageFromGitAttributes do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project, :repository) }
+
+ describe '#language_from_gitattributes' do
+ subject(:blob) { fake_blob(path: 'file.md') }
+
+ it 'returns return value from gitattribute' do
+ expect(blob.project.repository).to receive(:gitattribute).with(blob.path, 'gitlab-language').and_return('erb?parent=json')
+
+ expect(blob.language_from_gitattributes).to eq('erb?parent=json')
+ end
+
+ it 'returns nil if project is absent' do
+ allow(blob).to receive(:project).and_return(nil)
+
+ expect(blob.language_from_gitattributes).to eq(nil)
+ end
+ end
+end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index f8c2e29fadd..827fbc9d7d5 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -41,7 +41,7 @@ describe CacheableAttributes do
expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
end
- it 'can be overriden' do
+ it 'can be overridden' do
minimal_test_class.define_singleton_method(:current_without_cache) do
first
end
@@ -64,7 +64,7 @@ describe CacheableAttributes do
context 'with defaults defined' do
include_context 'with defaults'
- it 'can be overriden' do
+ it 'can be overridden' do
expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index ec6374f3963..a4bf3e2350a 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -519,7 +519,7 @@ describe Issuable do
end
end
- context 'substracting time' do
+ context 'subtracting time' do
before do
spend_time(1800)
end
@@ -530,7 +530,7 @@ describe Issuable do
expect(issue.total_time_spent).to eq(900)
end
- context 'when time to substract exceeds the total time spent' do
+ context 'when time to subtract exceeds the total time spent' do
it 'raise a validation error' do
Timecop.travel(1.minute.from_now) do
expect do
diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb
new file mode 100644
index 00000000000..7d320edd492
--- /dev/null
+++ b/spec/models/concerns/redactable_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Redactable do
+ shared_examples 'model with redactable field' do
+ it 'redacts unsubscribe token' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ end
+
+ it 'ignores not hexadecimal tokens' do
+ text = 'some text /sent_notifications/token/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'ignores not matching texts' do
+ text = 'some text /sent_notifications/.*/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'redacts the field when saving the model before creating markdown cache' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ expect(model[field]).to eq expected
+ expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
+ end
+ end
+
+ context 'when model is an issue' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:issue) }
+ let(:field) { :description }
+ end
+ end
+
+ context 'when model is a merge request' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:merge_request) }
+ let(:field) { :description }
+ end
+ end
+
+ context 'when model is a note' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:note) }
+ let(:field) { :note }
+ end
+ end
+
+ context 'when model is a snippet' do
+ it_behaves_like 'model with redactable field' do
+ let(:model) { create(:snippet) }
+ let(:field) { :description }
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9b804429138..782687516ae 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
shared_examples 'TokenAuthenticatable' do
describe 'dynamically defined methods' do
- it { expect(described_class).to be_private_method_defined(:generate_token) }
- it { expect(described_class).to be_private_method_defined(:write_new_token) }
it { expect(described_class).to respond_to("find_by_#{token_field}") }
it { is_expected.to respond_to("ensure_#{token_field}") }
it { is_expected.to respond_to("set_#{token_field}") }
@@ -66,13 +64,275 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
describe 'multiple token fields' do
- before do
+ before(:all) do
described_class.send(:add_authentication_token_field, :yet_another_token)
end
- describe '.token_fields' do
- subject { described_class.authentication_token_fields }
- it { is_expected.to include(:runners_registration_token, :yet_another_token) }
+ it { is_expected.to respond_to(:ensure_runners_registration_token) }
+ it { is_expected.to respond_to(:ensure_yet_another_token) }
+ end
+
+ describe 'setting same token field multiple times' do
+ subject { described_class.send(:add_authentication_token_field, :runners_registration_token) }
+
+ it 'raises error' do
+ expect {subject}.to raise_error(ArgumentError)
+ end
+ end
+end
+
+describe PersonalAccessToken, 'TokenAuthenticatable' do
+ let(:personal_access_token_name) { 'test-pat-01' }
+ let(:token_value) { 'token' }
+ let(:user) { create(:user) }
+ let(:personal_access_token) do
+ described_class.new(name: personal_access_token_name,
+ user_id: user.id,
+ scopes: [:api],
+ token: token,
+ token_digest: token_digest)
+ end
+
+ before do
+ allow(Devise).to receive(:friendly_token).and_return(token_value)
+ end
+
+ describe '.find_by_token' do
+ subject { PersonalAccessToken.find_by_token(token_value) }
+
+ before do
+ personal_access_token.save
+ end
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+
+ context 'token_digest does not exist' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'finds the token' do
+ expect(subject).not_to be_nil
+ expect(subject.name).to eql(personal_access_token_name)
+ end
+ end
+ end
+
+ describe '#set_token' do
+ let(:new_token_value) { 'new-token' }
+ subject { personal_access_token.set_token(new_token_value) }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'overwrites token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(new_token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token' do
+ subject { personal_access_token.ensure_token }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#ensure_token!' do
+ subject { personal_access_token.ensure_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to be_nil
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { token_value }
+ let(:token_digest) { nil }
+
+ it 'does not change token fields' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to eql(token_value)
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to be_nil
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+ end
+
+ describe '#reset_token!' do
+ subject { personal_access_token.reset_token! }
+
+ context 'token_digest already exists' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist but token does' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest and clears token' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest does not exist, nor token' do
+ let(:token) { nil }
+ let(:token_digest) { nil }
+
+ it 'creates new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token_digest exists and newly generated token would be the same' do
+ let(:token) { nil }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ context 'token exists and newly generated token would be the same' do
+ let(:token) { 'old-token' }
+ let(:token_digest) { nil }
+
+ before do
+ personal_access_token.save
+ allow(Devise).to receive(:friendly_token).and_return(
+ 'old-token', token_value, 'boom!')
+ end
+
+ it 'regenerates a new token_digest' do
+ subject
+
+ expect(personal_access_token.read_attribute('token')).to be_nil
+ expect(personal_access_token.token).to eql(token_value)
+ expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
+ end
end
end
end
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index f2eb263c98c..e7805d52d75 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -5,13 +5,15 @@ describe EnvironmentStatus do
let(:environment) { deployment.environment}
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
+ let(:sha) { deployment.sha }
- subject(:environment_status) { described_class.new(environment, merge_request) }
+ subject(:environment_status) { described_class.new(environment, merge_request, sha) }
it { is_expected.to delegate_method(:id).to(:environment) }
it { is_expected.to delegate_method(:name).to(:environment) }
it { is_expected.to delegate_method(:project).to(:environment) }
it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) }
+ it { is_expected.to delegate_method(:status).to(:deployment) }
describe '#project' do
subject { environment_status.project }
@@ -58,4 +60,32 @@ describe EnvironmentStatus do
)
end
end
+
+ describe '.for_merge_request' 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)
+
+ described_class.for_merge_request(merge_request, admin)
+ end
+ end
+
+ describe '.after_merge_request' do
+ let(:admin) { create(:admin) }
+ let(:pipeline) { create(:ci_pipeline, sha: sha) }
+
+ before 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)
+
+ described_class.after_merge_request(merge_request, admin)
+ end
+ end
end
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index ab58f5c5021..b6355455c1d 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,41 +92,6 @@ describe GlobalMilestone do
end
end
- describe '.states_count' do
- context 'when the projects have milestones' do
- before do
- create(:closed_milestone, title: 'Active Group Milestone', project: project3)
- create(:active_milestone, title: 'Active Group Milestone', project: project1)
- create(:active_milestone, title: 'Active Group Milestone', project: project2)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
- create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
- end
-
- it 'returns the quantity of global milestones in each possible state' do
- expected_count = { opened: 1, closed: 2, all: 2 }
-
- count = described_class.states_count(Project.all)
-
- expect(count).to eq(expected_count)
- end
- end
-
- context 'when the projects do not have milestones' do
- before do
- project1
- end
-
- it 'returns 0 as the quantity of global milestones in each state' do
- expected_count = { opened: 0, closed: 0, all: 0 }
-
- count = described_class.states_count(Project.all)
-
- expect(count).to eq(expected_count)
- end
- end
- end
-
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 1bf8f89e126..ada00f03928 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -19,6 +19,8 @@ describe Group do
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
+ it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
+ it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -651,10 +653,10 @@ describe Group do
end
end
- describe '#secret_variables_for' do
+ describe '#ci_variables_for' do
let(:project) { create(:project, group: group) }
- let!(:secret_variable) do
+ let!(:ci_variable) do
create(:ci_group_variable, value: 'secret', group: group)
end
@@ -662,11 +664,11 @@ describe Group do
create(:ci_group_variable, :protected, value: 'protected', group: group)
end
- subject { group.secret_variables_for('ref', project) }
+ subject { group.ci_variables_for('ref', project) }
shared_examples 'ref is protected' do
it 'contains all the variables' do
- is_expected.to contain_exactly(secret_variable, protected_variable)
+ is_expected.to contain_exactly(ci_variable, protected_variable)
end
end
@@ -676,8 +678,8 @@ describe Group do
default_branch_protection: Gitlab::Access::PROTECTION_NONE)
end
- it 'contains only the secret variables' do
- is_expected.to contain_exactly(secret_variable)
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(ci_variable)
end
end
@@ -710,9 +712,9 @@ describe Group do
end
it 'returns all variables belong to the group and parent groups' do
- expected_array1 = [protected_variable, secret_variable]
+ expected_array1 = [protected_variable, ci_variable]
expected_array2 = [variable_child, variable_child_2, variable_child_3]
- got_array = group_child_3.secret_variables_for('ref', project).to_a
+ got_array = group_child_3.ci_variables_for('ref', project).to_a
expect(got_array.shift(2)).to contain_exactly(*expected_array1)
expect(got_array).to eq(expected_array2)
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 6e35511e848..3f929710862 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -2,19 +2,13 @@ require 'spec_helper'
describe LfsObject do
describe '#local_store?' do
- it 'returns true when file_store is nil' do
- subject.file_store = nil
-
- expect(subject.local_store?).to eq true
- end
-
it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do
subject.file_store = LfsObjectUploader::Store::LOCAL
expect(subject.local_store?).to eq true
end
- it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do
+ it 'returns false when file_store is equal to LfsObjectUploader::Store::REMOTE' do
subject.file_store = LfsObjectUploader::Store::REMOTE
expect(subject.local_store?).to eq false
@@ -83,19 +77,6 @@ describe LfsObject do
describe 'file is being stored' do
let(:lfs_object) { create(:lfs_object, :with_file) }
- context 'when object has nil store' do
- before do
- lfs_object.update_column(:file_store, nil)
- lfs_object.reload
- end
-
- it 'is stored locally' do
- expect(lfs_object.file_store).to be(nil)
- expect(lfs_object.file).to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
-
context 'when existing object has local store' do
it 'is stored locally' do
expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 90cce826b6c..47e8f04e728 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -52,9 +52,9 @@ describe MergeRequestDiff do
context 'when it was not cleaned by the system' do
it 'returns persisted diffs' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
- diff.diffs
+ diff.diffs.diff_files
end
end
@@ -76,19 +76,19 @@ describe MergeRequestDiff do
end
it 'returns persisted diffs if cannot compare with diff refs' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
diff.update!(head_commit_sha: 'invalid-sha')
- diff.diffs
+ diff.diffs.diff_files
end
it 'returns persisted diffs if diff refs does not exist' do
- expect(diff).to receive(:load_diffs)
+ expect(diff).to receive(:load_diffs).and_call_original
diff.update!(start_commit_sha: nil, base_commit_sha: nil)
- diff.diffs
+ diff.diffs.diff_files
end
end
end
@@ -211,4 +211,25 @@ describe MergeRequestDiff do
expect(diff_with_commits.commits_count).to eq(29)
end
end
+
+ describe '#commits_by_shas' do
+ let(:commit_shas) { diff_with_commits.commit_shas }
+
+ it 'returns empty if no SHAs were provided' do
+ expect(diff_with_commits.commits_by_shas([])).to be_empty
+ end
+
+ it 'returns one SHA' do
+ commits = diff_with_commits.commits_by_shas([commit_shas.first, Gitlab::Git::BLANK_SHA])
+
+ expect(commits.count).to eq(1)
+ end
+
+ it 'returns all matching SHAs' do
+ commits = diff_with_commits.commits_by_shas(commit_shas)
+
+ expect(commits.count).to eq(commit_shas.count)
+ expect(commits.map(&:sha)).to match_array(commit_shas)
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 666d7e69f89..2eb5e39ccfd 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -13,6 +13,20 @@ describe MergeRequest do
it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:merge_request_diffs) }
+
+ context 'for forks' do
+ let!(:project) { create(:project) }
+ let!(:fork) { fork_project(project) }
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: fork) }
+
+ it 'does not load another project due to inverse relationship' do
+ expect(project.merge_requests.first.target_project.object_id).to eq(project.object_id)
+ end
+
+ it 'finds the associated merge request' do
+ expect(project.merge_requests.find(merge_request.id)).to eq(merge_request)
+ end
+ end
end
describe '#squash_in_progress?' do
@@ -538,9 +552,9 @@ describe MergeRequest do
it 'delegates to the MR diffs' do
merge_request.save
- expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
- merge_request.diffs(options)
+ merge_request.diffs(options).diff_files
end
end
@@ -1058,6 +1072,26 @@ describe MergeRequest do
end
end
+ describe '#merge_pipeline' do
+ it 'returns nil when not merged' do
+ expect(subject.merge_pipeline).to be_nil
+ end
+
+ context 'when the MR is merged' do
+ let(:sha) { subject.target_project.commit.id }
+ let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }
+
+ before do
+ subject.mark_as_merged!
+ subject.update_attribute(:merge_commit_sha, pipeline.sha)
+ end
+
+ it 'returns the post-merge pipeline' do
+ expect(subject.merge_pipeline).to eq(pipeline)
+ end
+ end
+ end
+
describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) }
@@ -2577,6 +2611,32 @@ describe MergeRequest do
end
end
+ describe '#includes_any_commits?' do
+ it 'returns false' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(subject.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ it 'returns true even when there is a non-existent comit' do
+ expect(subject.includes_any_commits?([Gitlab::Git::BLANK_SHA, subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+
+ context 'unpersisted merge request' do
+ let(:new_mr) { build(:merge_request) }
+
+ it 'returns false' do
+ expect(new_mr.includes_any_commits?([Gitlab::Git::BLANK_SHA])).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(new_mr.includes_any_commits?([subject.merge_request_diff.head_commit_sha])).to be_truthy
+ end
+ end
+ end
+
describe '#can_allow_collaboration?' do
let(:target_project) { create(:project, :public) }
let(:source_project) { fork_project(target_project) }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 27d4e622710..d11eb46159e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -348,4 +348,41 @@ describe Milestone do
end
end
end
+
+ describe '.states_count' do
+ context 'when the projects have milestones' do
+ before do
+ project_1 = create(:project)
+ project_2 = create(:project)
+ group_1 = create(:group)
+ group_2 = create(:group)
+
+ create(:active_milestone, title: 'Active Group Milestone', project: project_1)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
+ create(:active_milestone, title: 'Active Group Milestone', project: project_2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
+ create(:closed_milestone, title: 'Active Group Milestone', group: group_1)
+ create(:closed_milestone, title: 'Closed Group Milestone', group: group_1)
+ create(:closed_milestone, title: 'Active Group Milestone', group: group_2)
+ create(:closed_milestone, title: 'Closed Group Milestone', group: group_2)
+ end
+
+ it 'returns the quantity of milestones in each possible state' do
+ expected_count = { opened: 5, closed: 6, all: 11 }
+
+ count = described_class.states_count(Project.all, Group.all)
+ expect(count).to eq(expected_count)
+ end
+ end
+
+ context 'when the projects do not have milestones' do
+ it 'returns 0 as the quantity of global milestones in each state' do
+ expected_count = { opened: 0, closed: 0, all: 0 }
+
+ count = described_class.states_count([project])
+
+ expect(count).to eq(expected_count)
+ end
+ end
+ end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 2bb1c49b740..c82ab9c9e62 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -49,18 +49,36 @@ describe PersonalAccessToken do
describe 'Redis storage' do
let(:user_id) { 123 }
- let(:token) { 'abc000foo' }
+ let(:token) { 'KS3wegQYXBLYhQsciwsj' }
- before do
- subject.redis_store!(user_id, token)
+ context 'reading encrypted data' do
+ before do
+ subject.redis_store!(user_id, token)
+ end
+
+ it 'returns stored data' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
- it 'returns stored data' do
- expect(subject.redis_getdel(user_id)).to eq(token)
+ context 'reading unencrypted data' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(described_class.redis_shared_state_key(user_id),
+ token,
+ ex: PersonalAccessToken::REDIS_EXPIRY_TIME)
+ end
+ end
+
+ it 'returns stored data unmodified' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
end
context 'after deletion' do
before do
+ subject.redis_store!(user_id, token)
+
expect(subject.redis_getdel(user_id)).to eq(token)
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 0cd712e2f40..b0fd2ceead0 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -387,4 +387,22 @@ describe HipchatService do
end
end
end
+
+ context 'with UrlBlocker' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:hipchat) { described_class.new(project: project) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+
+ describe '#execute' do
+ before do
+ hipchat.server = 'http://localhost:9123'
+ end
+
+ it 'raises UrlBlocker for localhost' do
+ expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original
+ expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 68ab9fd08ec..9c27357ffaf 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -253,7 +253,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
end
- describe '#predefined_variables' do
+ describe '#predefined_variable' do
let(:kubeconfig) do
config_file = expand_fixture_path('config/kubeconfig.yml')
config = YAML.load(File.read(config_file))
@@ -274,7 +274,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
shared_examples 'setting variables' do
it 'sets the variables' do
- expect(subject.predefined_variables).to include(
+ expect(subject.predefined_variables(project: project)).to include(
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
{ key: 'KUBE_TOKEN', value: 'token', public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true },
@@ -301,7 +301,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it_behaves_like 'setting variables'
it 'sets the KUBE_NAMESPACE' do
- kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+ kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' }
expect(kube_namespace).not_to be_nil
expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 62a38c66d99..d059854214f 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -88,6 +88,10 @@ describe Project do
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
+ it 'has an inverse relationship with merge requests' do
+ expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
+ end
+
context 'after initialized' do
it "has a project_feature" do
expect(described_class.new.project_feature).to be_present
@@ -2401,12 +2405,24 @@ describe Project do
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
- context 'when user configured kubernetes from CI/CD > Clusters' do
+ context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has not been executed' do
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
+
+ context 'when user configured kubernetes from CI/CD > Clusters and KubernetesNamespace migration has been executed' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace) }
+ let!(:cluster) { kubernetes_namespace.cluster }
+ let(:project) { kubernetes_namespace.project }
+
+ it 'should return token from kubernetes namespace' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ )
+ end
+ end
end
end
@@ -2432,10 +2448,10 @@ describe Project do
end
end
- describe '#secret_variables_for' do
+ describe '#ci_variables_for' do
let(:project) { create(:project) }
- let!(:secret_variable) do
+ let!(:ci_variable) do
create(:ci_variable, value: 'secret', project: project)
end
@@ -2443,7 +2459,7 @@ describe Project do
create(:ci_variable, :protected, value: 'protected', project: project)
end
- subject { project.reload.secret_variables_for(ref: 'ref') }
+ subject { project.reload.ci_variables_for(ref: 'ref') }
before do
stub_application_setting(
@@ -2452,13 +2468,13 @@ describe Project do
shared_examples 'ref is protected' do
it 'contains all the variables' do
- is_expected.to contain_exactly(secret_variable, protected_variable)
+ is_expected.to contain_exactly(ci_variable, protected_variable)
end
end
context 'when the ref is not protected' do
- it 'contains only the secret variables' do
- is_expected.to contain_exactly(secret_variable)
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(ci_variable)
end
end
@@ -2746,7 +2762,7 @@ describe Project do
.to raise_error(ActiveRecord::RecordNotSaved, error_message)
end
- it 'updates the project succesfully' do
+ it 'updates the project successfully' do
merge_request = create(:merge_request, target_project: project, source_project: project)
expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }
@@ -3314,7 +3330,7 @@ describe Project do
end
end
- context 'when explicitely enabled' do
+ context 'when explicitly enabled' do
context 'when domain is empty' do
before do
create(:project_auto_devops, project: project, domain: nil)
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f38fc191943..cc5e34782ec 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -145,7 +145,7 @@ describe ProjectWiki do
end
it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existant")).to eq(nil)
+ expect(subject.find_page("non-existent")).to eq(nil)
end
it "can find a page by slug" do
@@ -226,7 +226,7 @@ describe ProjectWiki do
end
it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existant')).to eq(nil)
+ expect(subject.find_file('non-existent')).to eq(nil)
end
it 'returns a Gitlab::Git::WikiFile instance' do
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 36b8e5d304f..3c89e99abf0 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -91,7 +91,7 @@ describe Upload do
.to change { upload.checksum }.from(nil).to(expected)
end
- it 'sets `checksum` to nil for a non-existant file' do
+ it 'sets `checksum` to nil for a non-existent file' do
expect(upload).to receive(:exist?).and_return(false)
checksum = Digest::SHA256.file(__FILE__).hexdigest
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b3474e74aa4..4e7c8523e65 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -730,6 +730,14 @@ describe User do
expect(user.incoming_email_token).not_to be_blank
end
+
+ it 'uses SecureRandom to generate the incoming email token' do
+ expect(SecureRandom).to receive(:hex).and_return('3b8ca303')
+
+ user = create(:user)
+
+ expect(user.incoming_email_token).to eql('gitlab')
+ end
end
describe '#ensure_user_rights_and_limits' do
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
new file mode 100644
index 00000000000..e85e7a41017
--- /dev/null
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BlobPresenter, :seed_helper do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+
+ let(:git_blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/ruby/regex.rb'
+ )
+ end
+ let(:blob) { Blob.new(git_blob) }
+
+ describe '#highlight' do
+ subject { described_class.new(blob) }
+
+ it 'returns highlighted content' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil)
+
+ subject.highlight
+ end
+
+ it 'returns plain content when :plain is true' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil)
+
+ subject.highlight(plain: true)
+ end
+
+ context 'gitlab-language contains a match' do
+ before do
+ allow(blob).to receive(:language_from_gitattributes).and_return('ruby')
+ end
+
+ it 'passes language to inner call' do
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby')
+
+ subject.highlight
+ end
+ end
+ end
+end
diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..4f4ae5e07c5
--- /dev/null
+++ b/spec/presenters/clusterable_presenter_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ describe '.fabricate' do
+ let(:project) { create(:project) }
+
+ subject { described_class.fabricate(project) }
+
+ it 'creates an object from a descendant presenter' do
+ expect(subject).to be_kind_of(ProjectClusterablePresenter)
+ end
+ end
+end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index e96dbfb73c0..7af181f37d5 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Clusters::ClusterPresenter do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
+ include Gitlab::Routing.url_helpers
+
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
subject(:presenter) do
described_class.new(cluster)
@@ -71,4 +73,14 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(false) }
end
end
+
+ describe '#show_path' do
+ subject { described_class.new(cluster).show_path }
+
+ context 'project_type cluster' do
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq(project_cluster_path(project, cluster)) }
+ end
+ end
end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index a1b52d8692d..bafcddebbb7 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -403,6 +403,15 @@ describe MergeRequestPresenter do
is_expected
.to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>")
end
+
+ it 'escapes html, when source_branch does not exist' do
+ xss_attempt = "<img src='x' onerror=alert('bad stuff') />"
+
+ allow(resource).to receive(:source_branch) { xss_attempt }
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to eq(ERB::Util.html_escape(xss_attempt))
+ end
end
describe '#rebase_path' do
diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..c50d90ae1e8
--- /dev/null
+++ b/spec/presenters/project_clusterable_presenter_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(project) }
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ 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
+ project.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(project_clusters_path(project)) }
+ end
+
+ describe '#new_path' do
+ subject { presenter.new_path }
+
+ it { is_expected.to eq(new_project_cluster_path(project)) }
+ end
+
+ describe '#create_user_clusters_path' do
+ subject { presenter.create_user_clusters_path }
+
+ it { is_expected.to eq(create_user_project_clusters_path(project)) }
+ end
+
+ describe '#create_gcp_clusters_path' do
+ subject { presenter.create_gcp_clusters_path }
+
+ it { is_expected.to eq(create_gcp_project_clusters_path(project)) }
+ end
+
+ describe '#cluster_status_cluster_path' do
+ subject { presenter.cluster_status_cluster_path(cluster) }
+
+ it { is_expected.to eq(cluster_status_project_cluster_path(project, 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_project_cluster_path(project, cluster, application)) }
+ end
+
+ describe '#cluster_path' do
+ subject { presenter.cluster_path(cluster) }
+
+ it { is_expected.to eq(project_cluster_path(project, cluster)) }
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 3eb2f149311..7b0192fa9c8 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -239,7 +239,7 @@ describe ProjectPresenter do
expect(presenter.new_file_anchor_data).to have_attributes(enabled: false,
label: "New file",
link: presenter.project_new_blob_path(project, 'master'),
- class_modifier: 'new')
+ class_modifier: 'success')
end
it 'returns nil if user cannot push' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 98399471f9a..2963dea634a 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -565,7 +565,7 @@ describe API::Commits do
}
end
- it 'are commited as one in project repo' do
+ it 'are committed as one in project repo' do
post api(url, user), valid_mo_params
expect(response).to have_gitlab_http_status(201)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index e0b5b34f9c4..5ea869796b0 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -494,6 +494,24 @@ describe API::Internal do
end
end
+ context 'request times out' do
+ context 'git push' do
+ it 'responds with a gateway timeout' do
+ personal_project = create(:project, namespace: user.namespace)
+
+ expect_next_instance_of(Gitlab::GitAccess) do |access|
+ expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo")
+ end
+ push(key, personal_project)
+
+ expect(response).to have_gitlab_http_status(503)
+ expect(json_response['status']).to be_falsey
+ expect(json_response['message']).to eq("Foo")
+ expect(user.reload.last_activity_on).to be_nil
+ end
+ end
+ end
+
context "archived project" do
before do
project.add_developer(user)
@@ -665,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?merge_request%5Bsource_branch%5D=new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
"new_merge_request" => true
}]
end
@@ -686,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?merge_request%5Bsource_branch%5D=new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
"new_merge_request" => true
}]
end
@@ -819,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?merge_request%5Bsource_branch%5D=new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch",
"new_merge_request" => true
}]
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9cda39a569b..3d532dd83c7 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -55,8 +55,8 @@ describe API::Issues do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- let(:no_milestone_title) { URI.escape(Milestone::None.title) }
- let(:any_milestone_title) { URI.escape(Milestone::Any.title) }
+ let(:no_milestone_title) { "None" }
+ let(:any_milestone_title) { "Any" }
before(:all) do
project.add_reporter(user)
@@ -196,14 +196,24 @@ describe API::Issues do
expect_paginated_array_response(size: 3)
end
- it 'returns issues reacted by the authenticated user by the given emoji' do
+ it 'returns issues reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
- award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
- get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
+ create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
- expect_paginated_array_response(size: 1)
- expect(first_issue['id']).to eq(issue2.id)
+ get api('/issues', user2), my_reaction_emoji: 'Any', scope: 'all'
+
+ expect_paginated_array_response(size: 2)
+ end
+
+ it 'returns issues not reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ get api('/issues', user2), my_reaction_emoji: 'None', scope: 'all'
+
+ expect_paginated_array_response(size: 2)
end
it 'returns issues matching given search string for title' do
@@ -1791,6 +1801,74 @@ describe API::Issues do
end
end
+ describe 'GET :id/issues/:issue_iid/related_merge_requests' do
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: "master",
+ target_branch: "test",
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ let!(:related_mr) { create_referencing_mr(user, project, issue) }
+
+ context 'when unauthenticated' do
+ it 'return list of referenced merge requests from issue' do
+ get_related_merge_requests(project.id, issue.iid)
+
+ expect_paginated_array_response(size: 1)
+ end
+
+ it 'renders 404 if project is not visible' do
+ private_project = create(:project, :private)
+ private_issue = create(:issue, project: private_project)
+ create_referencing_mr(user, private_project, private_issue)
+
+ get_related_merge_requests(private_project.id, private_issue.iid)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns merge requests that mentioned a issue' do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Some description")
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(related_mr.id)
+ end
+
+ context 'no merge request mentioned a issue' do
+ it 'returns empty array' do
+ get_related_merge_requests(project.id, closed_issue.iid, user)
+
+ expect_paginated_array_response(size: 0)
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get_related_merge_requests(project.id, 999999, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index c0d5a3ad74b..909703a8d47 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -806,6 +806,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it { expect(job).to be_unknown_failure }
end
+
+ context 'when failure_reason is job_execution_timeout' do
+ before do
+ update_job(state: 'failed', failure_reason: 'job_execution_timeout')
+ job.reload
+ end
+
+ it { expect(job).to be_job_execution_timeout }
+ end
end
context 'when trace is given' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index c40d01e1a14..08bada44178 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -158,6 +158,16 @@ describe API::Wikis do
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is missing')
end
+
+ it 'responds with validation error on invalid temp file' do
+ payload[:file] = { tempfile: '/etc/hosts' }
+
+ post(api(url, user), payload)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response.size).to eq(1)
+ expect(json_response['error']).to eq('file is invalid')
+ end
end
describe 'GET /projects/:id/wikis' do
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 9e2bee2ee60..ea88951ebc6 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -26,6 +26,10 @@ describe BuildActionEntity do
context 'when job is scheduled' do
let(:job) { create(:ci_build, :scheduled) }
+ it 'returns scheduled' do
+ expect(subject[:scheduled]).to be_truthy
+ end
+
it 'returns scheduled_at' do
expect(subject[:scheduled_at]).to eq(job.scheduled_at)
end
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index 6894c65d639..1b4d8b70aa6 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
- let(:environment_status) { EnvironmentStatus.new(environment, merge_request) }
+ let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) }
let(:entity) { described_class.new(environment_status, request: request) }
subject { entity.as_json }
@@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) }
it { is_expected.to include(:changes) }
+ it { is_expected.to include(:status) }
it { is_expected.not_to include(:stop_url) }
it { is_expected.not_to include(:metrics_url) }
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 5fc27da4906..851b41a7f7e 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -117,6 +117,7 @@ describe JobEntity do
end
it 'contains scheduled_at' do
+ expect(subject[:scheduled]).to be_truthy
expect(subject[:scheduled_at]).to eq(job.scheduled_at)
end
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 5bf8aa7f23f..561421d5ac8 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do
end
end
+ describe 'merge_pipeline' do
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+
+ context 'when is merged' do
+ let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns merge_pipeline' do
+ pipeline.reload
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: request)
+ .as_json
+
+ expect(subject[:merge_pipeline]).to eq(pipeline_payload)
+ end
+
+ context 'when user cannot read pipelines on target project' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns nil' do
+ expect(subject[:merge_pipeline]).to be_nil
+ end
+ end
+ end
+ end
+
describe 'metrics' do
context 'when metrics record exists with merged data' do
before do
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 9a53b32394d..704685417bb 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -98,47 +98,19 @@ describe Ci::ProcessBuildService, '#execute' do
let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
- context 'when ci_enable_scheduled_build is enabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('scheduled')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when current status is success' do
+ let(:current_status) { 'success' }
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ it 'changes the build status' do
+ expect { subject }.to change { build.status }.to('scheduled')
end
end
- context 'when ci_enable_scheduled_build is disabled' do
- before do
- stub_feature_flags(ci_enable_scheduled_build: false)
- end
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('manual')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when current status is failed' do
+ let(:current_status) { 'failed' }
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ it 'does not change the build status' do
+ expect { subject }.to change { build.status }.to('skipped')
end
end
end
diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb
index 2c921dac238..be2aad33ef4 100644
--- a/spec/services/ci/run_scheduled_build_service_spec.rb
+++ b/spec/services/ci/run_scheduled_build_service_spec.rb
@@ -7,10 +7,6 @@ describe Ci::RunScheduledBuildService do
subject { described_class.new(project, user).execute(build) }
- before do
- stub_feature_flags(ci_enable_scheduled_build: true)
- end
-
context 'when user can update build' do
before do
project.add_developer(user)
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 3959295c13e..274880f2c49 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -5,18 +5,43 @@ describe Clusters::CreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
- subject { described_class.new(user, params).execute(project: project, access_token: access_token) }
+ subject { described_class.new(user, params).execute(access_token: access_token) }
context 'when provider is gcp' do
context 'when project has no clusters' do
context 'when correct params' do
- include_context 'valid cluster create params'
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a',
+ legacy_abac: 'true'
+ },
+ clusterable: project
+ }
+ end
include_examples 'create cluster service success'
end
context 'when invalid params' do
- include_context 'invalid cluster create params'
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ },
+ clusterable: project
+ }
+ end
include_examples 'create cluster service error'
end
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
index e2fa93904c5..55f123ee786 100644
--- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -24,7 +24,7 @@ describe Clusters::Gcp::FetchOperationService do
end
end
- context 'when suceeded to fetch operation' do
+ context 'when succeeded to fetch operation' do
before do
stub_cloud_platform_get_zone_operation(gcp_project_id, zone, operation_id)
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 0f484222228..7fbb6cf2cf5 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -1,156 +1,176 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe Clusters::Gcp::FinalizeCreationService do
+describe Clusters::Gcp::FinalizeCreationService, '#execute' do
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
- describe '#execute' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- let(:provider) { cluster.provider }
- let(:platform) { cluster.platform }
- let(:gcp_project_id) { provider.gcp_project_id }
- let(:zone) { provider.zone }
- let(:cluster_name) { cluster.name }
+ let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:provider) { cluster.provider }
+ let(:platform) { cluster.platform }
+ let(:endpoint) { '111.111.111.111' }
+ let(:api_url) { 'https://' + endpoint }
+ let(:username) { 'sample-username' }
+ let(:password) { 'sample-password' }
+ let(:secret_name) { 'gitlab-token' }
+ let(:token) { 'sample-token' }
+ let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" }
- subject { described_class.new.execute(provider) }
+ subject { described_class.new.execute(provider) }
- shared_examples 'success' do
- it 'configures provider and kubernetes' do
- subject
+ shared_examples 'success' do
+ it 'configures provider and kubernetes' do
+ subject
- expect(provider).to be_created
- end
+ expect(provider).to be_created
end
- shared_examples 'error' do
- it 'sets an error to provider object' do
- subject
+ it 'properly configures database models' do
+ subject
- expect(provider.reload).to be_errored
- end
+ cluster.reload
+
+ 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.username).to eq(username)
+ expect(platform.password).to eq(password)
+ expect(platform.token).to eq(token)
+ end
+
+ it 'creates kubernetes namespace model' do
+ subject
+
+ kubernetes_namespace = cluster.reload.kubernetes_namespace
+ expect(kubernetes_namespace).to be_persisted
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.service_account_token).to be_present
end
+ end
+
+ shared_examples 'error' do
+ it 'sets an error to provider object' do
+ subject
- context 'when suceeded to fetch gke cluster info' do
- let(:endpoint) { '111.111.111.111' }
- let(:api_url) { 'https://' + endpoint }
- let(:username) { 'sample-username' }
- let(:password) { 'sample-password' }
- let(:secret_name) { 'gitlab-token' }
+ expect(provider.reload).to be_errored
+ end
+ end
+ shared_examples 'kubernetes information not successfully fetched' do
+ context 'when failed to fetch gke cluster info' do
before do
- stub_cloud_platform_get_zone_cluster(
- gcp_project_id, zone, cluster_name,
- {
- endpoint: endpoint,
- username: username,
- password: password
- }
- )
+ stub_cloud_platform_get_zone_cluster_error(provider.gcp_project_id, provider.zone, cluster.name)
end
- context 'service account and token created' do
- before do
- stub_kubeclient_discover(api_url)
- stub_kubeclient_create_service_account(api_url)
- stub_kubeclient_create_secret(api_url)
- end
-
- shared_context 'kubernetes token successfully fetched' do
- let(:token) { 'sample-token' }
-
- before do
- stub_kubeclient_get_secret(
- api_url,
- {
- metadata_name: secret_name,
- token: Base64.encode64(token)
- } )
- end
- end
-
- context 'provider legacy_abac is enabled' do
- include_context 'kubernetes token successfully fetched'
-
- it_behaves_like 'success'
-
- it 'properly configures database models' do
- subject
-
- cluster.reload
-
- 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.username).to eq(username)
- expect(platform.password).to eq(password)
- expect(platform).to be_abac
- expect(platform.authorization_type).to eq('abac')
- expect(platform.token).to eq(token)
- end
- end
-
- context 'provider legacy_abac is disabled' do
- before do
- provider.legacy_abac = false
- end
-
- include_context 'kubernetes token successfully fetched'
-
- context 'cluster role binding created' do
- before do
- stub_kubeclient_create_cluster_role_binding(api_url)
- end
-
- it_behaves_like 'success'
-
- it 'properly configures database models' do
- subject
-
- cluster.reload
-
- 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.username).to eq(username)
- expect(platform.password).to eq(password)
- expect(platform).to be_rbac
- expect(platform.token).to eq(token)
- end
- end
- end
-
- context 'when token is empty' do
- before do
- stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name)
- end
-
- it_behaves_like 'error'
- end
-
- context 'when failed to fetch kubernetes token' do
- before do
- stub_kubeclient_get_secret_error(api_url, secret_name)
- end
-
- it_behaves_like 'error'
- end
-
- context 'when service account fails to create' do
- before do
- stub_kubeclient_create_service_account_error(api_url)
- end
-
- it_behaves_like 'error'
- end
+ it_behaves_like 'error'
+ end
+
+ context 'when token is empty' do
+ let(:token) { '' }
+
+ it_behaves_like 'error'
+ end
+
+ context 'when failed to fetch kubernetes token' do
+ before do
+ stub_kubeclient_get_secret_error(api_url, secret_name, namespace: 'default')
end
+
+ it_behaves_like 'error'
end
- context 'when failed to fetch gke cluster info' do
+ context 'when service account fails to create' do
before do
- stub_cloud_platform_get_zone_cluster_error(gcp_project_id, zone, cluster_name)
+ stub_kubeclient_create_service_account_error(api_url, namespace: 'default')
end
it_behaves_like 'error'
end
end
+
+ shared_context 'kubernetes information successfully fetched' do
+ before do
+ stub_cloud_platform_get_zone_cluster(
+ provider.gcp_project_id, provider.zone, cluster.name,
+ {
+ endpoint: endpoint,
+ username: username,
+ password: password
+ }
+ )
+
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_create_namespace(api_url)
+ stub_kubeclient_create_service_account(api_url)
+ stub_kubeclient_create_secret(api_url)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: secret_name,
+ token: Base64.encode64(token),
+ namespace: 'default'
+ }
+ )
+
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: "#{namespace}-token",
+ token: Base64.encode64(token),
+ namespace: namespace
+ }
+ )
+ end
+ end
+
+ context 'With a legacy ABAC cluster' do
+ before do
+ provider.legacy_abac = true
+ end
+
+ include_context 'kubernetes information successfully fetched'
+
+ it_behaves_like 'success'
+
+ it 'uses ABAC authorization type' do
+ subject
+ cluster.reload
+
+ expect(platform).to be_abac
+ expect(platform.authorization_type).to eq('abac')
+ end
+
+ it_behaves_like 'kubernetes information not successfully fetched'
+ end
+
+ context 'With an RBAC cluster' do
+ before do
+ provider.legacy_abac = false
+
+ stub_kubeclient_create_cluster_role_binding(api_url)
+ stub_kubeclient_create_role_binding(api_url, namespace: namespace)
+ end
+
+ include_context 'kubernetes information successfully fetched'
+
+ it_behaves_like 'success'
+
+ it 'uses RBAC authorization type' do
+ subject
+ cluster.reload
+
+ expect(platform).to be_rbac
+ expect(platform.authorization_type).to eq('rbac')
+ end
+
+ it_behaves_like 'kubernetes information not successfully fetched'
+ end
end
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
new file mode 100644
index 00000000000..fc922218ad0
--- /dev/null
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
+ include KubernetesHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:platform) { cluster.platform }
+ let(:api_url) { 'https://kubernetes.example.com' }
+ let(:project) { cluster.project }
+ let(:cluster_project) { cluster.cluster_project }
+
+ subject do
+ described_class.new(
+ cluster: cluster,
+ kubernetes_namespace: kubernetes_namespace
+ ).execute
+ end
+
+ shared_context 'kubernetes requests' do
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url)
+ stub_kubeclient_create_service_account(api_url)
+ stub_kubeclient_create_secret(api_url)
+
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace)
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: "#{namespace}-token",
+ token: Base64.encode64('sample-token'),
+ namespace: namespace
+ }
+ )
+ end
+ end
+
+ context 'when kubernetes namespace is not persisted' do
+ let(:namespace) { "#{project.path}-#{project.id}" }
+
+ let(:kubernetes_namespace) do
+ build(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
+
+ include_context 'kubernetes requests'
+
+ it 'creates a Clusters::KubernetesNamespace' do
+ expect do
+ subject
+ end.to change(Clusters::KubernetesNamespace, :count).by(1)
+ end
+
+ it 'creates project service account' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+
+ subject
+ end
+
+ it 'configures kubernetes token' do
+ subject
+
+ kubernetes_namespace.reload
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ end
+ end
+
+ context 'when there is a Kubernetes Namespace associated' do
+ let(:namespace) { 'new-namespace' }
+
+ let(:kubernetes_namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ project: cluster_project.project,
+ cluster_project: cluster_project)
+ end
+
+ include_context 'kubernetes requests'
+
+ before do
+ platform.update_column(:namespace, 'new-namespace')
+ end
+
+ it 'does not create any Clusters::KubernetesNamespace' do
+ subject
+
+ expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace)
+ end
+
+ it 'creates project service account' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateServiceAccountService).to receive(:execute).once
+
+ subject
+ end
+
+ it 'updates Clusters::KubernetesNamespace' do
+ subject
+
+ kubernetes_namespace.reload
+
+ expect(kubernetes_namespace.namespace).to eq(namespace)
+ expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account")
+ expect(kubernetes_namespace.encrypted_service_account_token).to be_present
+ end
+ end
+end
diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
index b096f1fa4fb..588edff85d4 100644
--- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
@@ -1,94 +1,165 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
include KubernetesHelpers
- let(:service) { described_class.new(kubeclient, rbac: rbac) }
+ let(:api_url) { 'http://111.111.111.111' }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+ let(:cluster_project) { cluster.cluster_project }
+ let(:project) { cluster_project.project }
+ let(:cluster) do
+ create(:cluster,
+ :project, :provided_by_gcp,
+ platform_kubernetes: create(:cluster_platform_kubernetes, :configured))
+ end
+
+ let(:kubeclient) do
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ auth_options: { username: 'admin', password: 'xxx' }
+ )
+ end
- describe '#execute' do
- let(:rbac) { false }
- let(:api_url) { 'http://111.111.111.111' }
- let(:username) { 'admin' }
- let(:password) { 'xxx' }
+ shared_examples 'creates service account and token' do
+ it 'creates a kubernetes service account' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts").with(
+ body: hash_including(
+ kind: 'ServiceAccount',
+ metadata: { name: service_account_name, namespace: namespace }
+ )
+ )
+ end
- let(:kubeclient) do
- Gitlab::Kubernetes::KubeClient.new(
- api_url,
- auth_options: { username: username, password: password }
+ it 'creates a kubernetes secret' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets").with(
+ body: hash_including(
+ kind: 'Secret',
+ metadata: {
+ name: token_name,
+ namespace: namespace,
+ annotations: {
+ 'kubernetes.io/service-account.name': service_account_name
+ }
+ },
+ type: 'kubernetes.io/service-account-token'
+ )
)
end
+ end
+
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_namespace(api_url, namespace: namespace)
+ stub_kubeclient_create_service_account(api_url, namespace: namespace )
+ stub_kubeclient_create_secret(api_url, namespace: namespace)
+ end
+
+ describe '.gitlab_creator' do
+ let(:namespace) { 'default' }
+ let(:service_account_name) { 'gitlab' }
+ let(:token_name) { 'gitlab-token' }
+
+ subject { described_class.gitlab_creator(kubeclient, rbac: rbac).execute }
+
+ context 'with ABAC cluster' do
+ let(:rbac) { false }
+
+ it_behaves_like 'creates service account and token'
+ end
- subject { service.execute }
+ context 'with RBAC cluster' do
+ let(:rbac) { true }
- context 'when params are correct' do
before do
- stub_kubeclient_discover(api_url)
- stub_kubeclient_create_service_account(api_url)
- stub_kubeclient_create_secret(api_url)
- end
+ cluster.platform_kubernetes.rbac!
- shared_examples 'creates service account and token' do
- it 'creates a kubernetes service account' do
- subject
+ stub_kubeclient_create_cluster_role_binding(api_url)
+ end
- expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with(
- body: hash_including(
- kind: 'ServiceAccount',
- metadata: { name: 'gitlab', namespace: 'default' }
- )
- )
- end
-
- it 'creates a kubernetes secret of type ServiceAccountToken' do
- subject
-
- expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with(
- body: hash_including(
- kind: 'Secret',
- metadata: {
- name: 'gitlab-token',
- namespace: 'default',
- annotations: {
- 'kubernetes.io/service-account.name': 'gitlab'
- }
- },
- type: 'kubernetes.io/service-account-token'
- )
+ it_behaves_like 'creates service account and token'
+
+ it 'should create a cluster role binding with cluster-admin access' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
+ body: hash_including(
+ kind: 'ClusterRoleBinding',
+ metadata: { name: 'gitlab-admin' },
+ roleRef: {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'ClusterRole',
+ name: 'cluster-admin'
+ },
+ subjects: [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
)
- end
+ )
end
+ end
+ end
+
+ describe '.namespace_creator' do
+ let(:namespace) { "#{project.path}-#{project.id}" }
+ let(:service_account_name) { "#{namespace}-service-account" }
+ let(:token_name) { "#{namespace}-token" }
+
+ subject do
+ described_class.namespace_creator(
+ kubeclient,
+ service_account_name: service_account_name,
+ service_account_namespace: namespace,
+ rbac: rbac
+ ).execute
+ end
+
+ context 'with ABAC cluster' do
+ let(:rbac) { false }
+
+ it_behaves_like 'creates service account and token'
+ end
+
+ context 'With RBAC enabled cluster' do
+ let(:rbac) { true }
+
+ before do
+ cluster.platform_kubernetes.rbac!
- context 'abac enabled cluster' do
- it_behaves_like 'creates service account and token'
+ stub_kubeclient_create_role_binding(api_url, namespace: namespace)
end
- context 'rbac enabled cluster' do
- let(:rbac) { true }
-
- before do
- stub_kubeclient_create_cluster_role_binding(api_url)
- end
-
- it_behaves_like 'creates service account and token'
-
- it 'creates a kubernetes cluster role binding' do
- subject
-
- expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with(
- body: hash_including(
- kind: 'ClusterRoleBinding',
- metadata: { name: 'gitlab-admin' },
- roleRef: {
- apiGroup: 'rbac.authorization.k8s.io',
- kind: 'ClusterRole',
- name: 'cluster-admin'
- },
- subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }]
- )
+ it_behaves_like 'creates service account and token'
+
+ it 'creates a namespaced role binding with edit access' do
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with(
+ body: hash_including(
+ kind: 'RoleBinding',
+ metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
+ roleRef: {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'ClusterRole',
+ name: 'edit'
+ },
+ subjects: [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
)
- end
+ )
end
end
end
diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
index 2355827fa5a..4d1a6bb7b3a 100644
--- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
@@ -1,56 +1,48 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
+ include KubernetesHelpers
+
describe '#execute' do
let(:api_url) { 'http://111.111.111.111' }
- let(:username) { 'admin' }
- let(:password) { 'xxx' }
+ let(:namespace) { 'my-namespace' }
+ let(:service_account_token_name) { 'gitlab-token' }
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
- auth_options: { username: username, password: password }
+ auth_options: { username: 'admin', password: 'xxx' }
)
end
- subject { described_class.new(kubeclient).execute }
+ subject { described_class.new(kubeclient, service_account_token_name, namespace).execute }
context 'when params correct' do
let(:decoded_token) { 'xxx.token.xxx' }
let(:token) { Base64.encode64(decoded_token) }
- let(:secret_json) do
- {
- 'metadata': {
- name: 'gitlab-token'
- },
- 'data': {
- 'token': token
- }
- }
- end
-
- before do
- allow_any_instance_of(Kubeclient::Client)
- .to receive(:get_secret).and_return(secret_json)
- end
-
context 'when gitlab-token exists' do
- let(:metadata_name) { 'gitlab-token' }
+ before do
+ stub_kubeclient_discover(api_url)
+ stub_kubeclient_get_secret(
+ api_url,
+ {
+ metadata_name: service_account_token_name,
+ namespace: namespace,
+ token: token
+ }
+ )
+ end
it { is_expected.to eq(decoded_token) }
end
context 'when gitlab-token does not exist' do
- let(:secret_json) { {} }
-
- it { is_expected.to be_nil }
- end
-
- context 'when token is nil' do
- let(:token) { nil }
+ before do
+ allow(kubeclient).to receive(:get_secret).and_raise(Kubeclient::HttpError.new(404, 'Not found', nil))
+ end
it { is_expected.to be_nil }
end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
index f48afdc83b2..c0bdac40938 100644
--- a/spec/services/clusters/gcp/provision_service_spec.rb
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -26,7 +26,7 @@ describe Clusters::Gcp::ProvisionService do
end
end
- context 'when suceeded to request provision' do
+ context 'when succeeded to request provision' do
before do
stub_cloud_platform_create_cluster(gcp_project_id, zone)
end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index dcd75b6912d..a1b20c61116 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Clusters::UpdateService do
+ include KubernetesHelpers
+
describe '#execute' do
subject { described_class.new(cluster.user, params).execute(cluster) }
@@ -34,6 +36,11 @@ describe Clusters::UpdateService do
}
end
+ before do
+ allow(ClusterPlatformConfigureWorker).to receive(:perform_async)
+ stub_kubeclient_get_namespace('https://kubernetes.example.com', namespace: 'my-namespace')
+ end
+
it 'updates namespace' do
is_expected.to eq(true)
expect(cluster.platform.namespace).to eq('custom-namespace')
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index d71ccfb4334..dd8a1cee074 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -177,7 +177,7 @@ describe Groups::TransferService, :postgresql do
it 'should add an error on group' do
transfer_service.execute(new_parent_group)
- expect(transfer_service.error).to eq('Transfer failed: Validation failed: Path has already been taken')
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
end
@@ -347,7 +347,7 @@ describe Groups::TransferService, :postgresql do
end
end
- context 'when transfering a group with nested groups and projects' do
+ context 'when transferring a group with nested groups and projects' do
let!(:group) { create(:group, :public) }
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 07aa8449a66..bd519e7f077 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -343,7 +343,42 @@ describe Issues::UpdateService, :mailer do
end
end
- context 'when the milestone change' do
+ context 'when the milestone is removed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ issue.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
+ it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ issue.milestone = create(:milestone)
+
+ issue.save
+
+ perform_enqueued_jobs do
+ update_issue(milestone_id: "")
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+
+ context 'when the milestone is changed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ issue.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
it 'marks todos as done' do
update_issue(milestone: create(:milestone))
@@ -351,6 +386,15 @@ describe Issues::UpdateService, :mailer do
end
it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ perform_enqueued_jobs do
+ update_issue(milestone: create(:milestone))
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
end
context 'when the labels change' do
@@ -374,7 +418,7 @@ describe Issues::UpdateService, :mailer do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) do
- create(:user).tap do |u|
+ create(:user) do |u|
label.toggle_subscription(u, project)
project.add_developer(u)
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 274624aa8bb..3e33a165e55 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?merge_request%5Bsource_branch%5D=#{source_branch}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{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?merge_request%5Bsource_branch%5D=new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/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/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2536c6e2514..61c6ba7d550 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -306,6 +306,66 @@ describe MergeRequests::RefreshService do
end
end
+ context 'forked projects with the same source branch name as target branch' do
+ let!(:first_commit) do
+ @fork_project.repository.create_file(@user, 'test1.txt', 'Test data',
+ message: 'Test commit',
+ branch_name: 'master')
+ end
+ let!(:second_commit) do
+ @fork_project.repository.create_file(@user, 'test2.txt', 'More test data',
+ message: 'Second test commit',
+ branch_name: 'master')
+ end
+ let!(:forked_master_mr) do
+ create(:merge_request,
+ source_project: @fork_project,
+ source_branch: 'master',
+ target_branch: 'master',
+ target_project: @project)
+ end
+ let(:force_push_commit) { @project.commit('feature').id }
+
+ it 'should reload a new diff for a push to the forked project' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a force push to the source branch' do
+ expect do
+ service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a force push to the target branch' do
+ expect do
+ service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should reload a new diff for a push to the target project that contains a commit in the MR' do
+ expect do
+ service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
+ reload_mrs
+ end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
+ end
+
+ it 'should not increase the diff count for a new push to target branch' do
+ new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file',
+ message: 'This is a test',
+ branch_name: 'master')
+
+ expect do
+ service.new(@project, @user).execute(@newrev, new_commit, 'refs/heads/master')
+ reload_mrs
+ end.not_to change { forked_master_mr.merge_request_diffs.count }
+ end
+ end
+
context 'push to origin repo target branch after fork project was removed' do
before do
@fork_project.destroy
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 21f369a3818..546c9f277c5 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -60,6 +60,17 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin
subject.execute
end
+
+ it 'avoids N+1 queries', :request_store do
+ current_user
+ merge_request
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.execute
+ end.count
+
+ expect { subject.execute }.not_to exceed_query_limit(control_count)
+ end
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 55dfab81c26..1b599ba11b6 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -315,7 +315,42 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- context 'when the milestone change' do
+ context 'when the milestone is removed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ merge_request.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
+ it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ merge_request.milestone = create(:milestone)
+
+ merge_request.save
+
+ perform_enqueued_jobs do
+ update_merge_request(milestone_id: "")
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
+ end
+
+ context 'when the milestone is changed' do
+ let!(:non_subscriber) { create(:user) }
+
+ let!(:subscriber) do
+ create(:user) do |u|
+ merge_request.toggle_subscription(u, project)
+ project.add_developer(u)
+ end
+ end
+
it 'marks pending todos as done' do
update_merge_request({ milestone: create(:milestone) })
@@ -323,6 +358,15 @@ describe MergeRequests::UpdateService, :mailer do
end
it_behaves_like 'system notes for milestones'
+
+ it 'sends notifications for subscribers of changed milestone' do
+ perform_enqueued_jobs do
+ update_merge_request(milestone: create(:milestone))
+ end
+
+ should_email(subscriber)
+ should_not_email(non_subscriber)
+ end
end
context 'when the labels change' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 68a361fa882..2d8da7673dc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -13,6 +13,54 @@ describe NotificationService, :mailer do
end
end
+ shared_examples 'altered milestone notification on issue' do
+ it 'sends the email to the correct people' do
+ should_email(subscriber_to_new_milestone)
+ issue.assignees.each do |a|
+ should_email(a)
+ end
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@subscribed_participant)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_committer)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ should_not_email(issue.author)
+ should_not_email(@u_disabled)
+ should_not_email(@u_custom_global)
+ should_not_email(@u_mentioned)
+ end
+ end
+
+ shared_examples 'altered milestone notification on merge request' do
+ it 'sends the email to the correct people' do
+ should_email(subscriber_to_new_milestone)
+ merge_request.assignees.each do |a|
+ should_email(a)
+ end
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@subscribed_participant)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_committer)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ should_not_email(merge_request.author)
+ should_not_email(@u_disabled)
+ should_not_email(@u_custom_global)
+ should_not_email(@u_mentioned)
+ end
+ end
+
shared_examples 'notifications for new mentions' do
it 'sends no emails when no new mentions are present' do
send_notifications
@@ -952,6 +1000,96 @@ describe NotificationService, :mailer do
end
end
+ describe '#removed_milestone_issue' do
+ it_behaves_like 'altered milestone notification on issue' do
+ let(:milestone) { create(:milestone, project: project, issues: [issue]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
+
+ before do
+ notification.removed_milestone_issue(issue, issue.author)
+ end
+ end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
+ let(:milestone) { create(:milestone, project: project, issues: [confidential_issue]) }
+
+ it "emails subscribers of the issue's milestone that can read the issue" do
+ project.add_developer(member)
+ project.add_guest(guest)
+
+ confidential_issue.subscribe(non_member, project)
+ confidential_issue.subscribe(author, project)
+ confidential_issue.subscribe(assignee, project)
+ confidential_issue.subscribe(member, project)
+ confidential_issue.subscribe(guest, project)
+ confidential_issue.subscribe(admin, project)
+
+ reset_delivered_emails!
+
+ notification.removed_milestone_issue(confidential_issue, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(guest)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+ end
+
+ describe '#changed_milestone_issue' do
+ it_behaves_like 'altered milestone notification on issue' do
+ let(:new_milestone) { create(:milestone, project: project, issues: [issue]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
+
+ before do
+ notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ end
+ end
+
+ context 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) }
+ let(:new_milestone) { create(:milestone, project: project, issues: [confidential_issue]) }
+
+ it "emails subscribers of the issue's milestone that can read the issue" do
+ project.add_developer(member)
+ project.add_guest(guest)
+
+ confidential_issue.subscribe(non_member, project)
+ confidential_issue.subscribe(author, project)
+ confidential_issue.subscribe(assignee, project)
+ confidential_issue.subscribe(member, project)
+ confidential_issue.subscribe(guest, project)
+ confidential_issue.subscribe(admin, project)
+
+ reset_delivered_emails!
+
+ notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled)
+
+ should_not_email(non_member)
+ should_not_email(guest)
+ should_email(author)
+ should_email(assignee)
+ should_email(member)
+ should_email(admin)
+ end
+ end
+ end
+
describe '#close_issue' do
before do
update_custom_notification(:close_issue, @u_guest_custom, resource: project)
@@ -1304,6 +1442,28 @@ describe NotificationService, :mailer do
end
end
+ describe '#removed_milestone_merge_request' do
+ it_behaves_like 'altered milestone notification on merge request' do
+ let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+
+ before do
+ notification.removed_milestone_merge_request(merge_request, merge_request.author)
+ end
+ end
+ end
+
+ describe '#changed_milestone_merge_request' do
+ it_behaves_like 'altered milestone notification on merge request' do
+ let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+
+ before do
+ notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author)
+ end
+ end
+ end
+
describe '#merge_request_unmergeable' do
it "sends email to merge request author" do
notification.merge_request_unmergeable(merge_request)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index e6ffa2b957b..06f865dc848 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -125,7 +125,7 @@ describe Projects::ImportService do
project.import_type = 'bitbucket'
end
- it 'succeeds if repository import is successfull' do
+ it 'succeeds if repository import is successful' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({})
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1411723fb9e..2e07d4f8013 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -125,7 +125,7 @@ describe Projects::TransferService do
it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' }
end
- context 'disallow transfering of project with tags' do
+ context 'disallow transferring of project with tags' do
let(:container_repository) { create(:container_repository) }
before do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 41a170e4f25..e513ee7ae44 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -315,7 +315,7 @@ describe QuickActions::InterpretService do
end
shared_examples 'award command' do
- it 'toggle award 100 emoji if content containts /award :100:' do
+ it 'toggle award 100 emoji if content contains /award :100:' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(emoji_award: "100")
@@ -1395,7 +1395,7 @@ describe QuickActions::InterpretService do
it 'includes the formatted duration and proper verb' do
_, explanations = service.explain(content, issue)
- expect(explanations).to eq(['Substracts 2h spent time.'])
+ expect(explanations).to eq(['Subtracts 2h spent time.'])
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index c0ceb0f6605..18a7a392c12 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -100,7 +100,7 @@ RSpec.configure do |config|
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
- # calling it explicitely here, we prevent any new requests from being fired
+ # calling it explicitly here, we prevent any new requests from being fired
# See https://github.com/teamcapybara/capybara/blob/ffb41cfad620de1961bb49b1562a9fa9b28c0903/lib/capybara/rspec.rb#L20-L25
# We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
Capybara.reset_sessions! unless example.exception
diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index f7f851eb1eb..bce1fb01355 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -5,7 +5,7 @@ shared_examples 'variable list' do
end
end
- it 'adds new secret variable' do
+ it 'adds new CI variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key value')
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 5f42ff77fb2..6569feec39b 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -120,8 +120,12 @@ module FilteredSearchHelpers
create_token('Label', label_name, symbol)
end
- def emoji_token(emoji_name = nil)
- { name: 'My-Reaction', emoji_name: emoji_name }
+ def reaction_token(reaction_name = nil, is_emoji = true)
+ if is_emoji
+ { name: 'My-Reaction', emoji_name: reaction_name }
+ else
+ create_token('My-Reaction', reaction_name)
+ end
end
def default_placeholder
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 6a7132c3093..9a86560da2a 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -35,7 +35,7 @@ module ProjectForksHelper
if create_repository
# The call to project.repository.after_import in RepositoryForkWorker does
# not reset the @exists variable of this forked_project.repository
- # so we have to explicitely call this method to clear the @exists variable.
+ # so we have to explicitly call this method to clear the @exists variable.
# of the instance we're returning here.
forked_project.repository.after_import
end
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
index 25781f5e679..90d7f60fdeb 100644
--- a/spec/support/helpers/seed_helper.rb
+++ b/spec/support/helpers/seed_helper.rb
@@ -1,15 +1,18 @@
+# frozen_string_literal: true
+
require_relative 'test_env'
# This file is specific to specs in spec/lib/gitlab/git/
SEED_STORAGE_PATH = TestEnv.repos_path
-TEST_REPO_PATH = 'gitlab-git-test.git'.freeze
-TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'.freeze
-TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze
-TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze
+TEST_REPO_PATH = 'gitlab-git-test.git'
+TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'
+TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'
+TEST_BROKEN_REPO_PATH = 'broken-repo.git'
+TEST_GITATTRIBUTES_REPO_PATH = 'with-git-attributes.git'
module SeedHelper
- GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
+ GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__)
def ensure_seeds
if File.exist?(SEED_STORAGE_PATH)
@@ -66,6 +69,11 @@ module SeedHelper
end
def create_git_attributes
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_GITATTRIBUTES_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info')
FileUtils.mkdir_p(dir)
@@ -82,6 +90,8 @@ foo/bar.* foo
*.cgi key=value?p1=v1&p2=v2
/*.png gitlab-language=png
*.binary binary
+/custom-highlighting/*.gitlab-custom gitlab-language=ruby
+/custom-highlighting/*.gitlab-cgi gitlab-language=erb?parent=json
# This uses a tab instead of spaces to ensure the parser also supports this.
*.md\tgitlab-language=markdown
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 7038a366144..9373de5aeab 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 ef9bb7f5533..361d4220c6e 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -3,12 +3,6 @@ shared_examples 'helm commands' do
let(:helm_setup) do
<<~EOS
set -eo pipefail
- ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
- echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U wget ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.2-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
EOS
end
diff --git a/spec/support/shared_examples/services/boards/lists_move_service.rb b/spec/support/shared_examples/services/boards/lists_move_service.rb
index 07c98cb29b7..2cdb968a45d 100644
--- a/spec/support/shared_examples/services/boards/lists_move_service.rb
+++ b/spec/support/shared_examples/services/boards/lists_move_service.rb
@@ -14,7 +14,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is equal to old position' do
+ it 'keeps position of lists when new position is equal to old position' do
service = described_class.new(parent, user, position: planning.position)
service.execute(planning)
@@ -22,7 +22,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is negative' do
+ it 'keeps position of lists when new position is negative' do
service = described_class.new(parent, user, position: -1)
service.execute(planning)
@@ -30,7 +30,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is equal to number of labels lists' do
+ it 'keeps position of lists when new position is equal to number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size)
service.execute(planning)
@@ -38,7 +38,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'keeps position of lists when new positon is greater than number of labels lists' do
+ it 'keeps position of lists when new position is greater than number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size + 1)
service.execute(planning)
@@ -46,7 +46,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [0, 1, 2, 3]
end
- it 'increments position of intermediate lists when new positon is equal to first position' do
+ it 'increments position of intermediate lists when new position is equal to first position' do
service = described_class.new(parent, user, position: 0)
service.execute(staging)
@@ -54,7 +54,7 @@ shared_examples 'lists move service' do
expect(current_list_positions).to eq [1, 2, 3, 0]
end
- it 'decrements position of intermediate lists when new positon is equal to last position' do
+ it 'decrements position of intermediate lists when new position is equal to last position' do
service = described_class.new(parent, user, position: board.lists.label.last.position)
service.execute(planning)
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 3ba6caf1337..8c4360d4cf0 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -251,7 +251,7 @@ describe 'gitlab:app namespace rake task' do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- # Avoid asking gitaly about the root ref (which will fail beacuse of the
+ # Avoid asking gitaly about the root ref (which will fail because of the
# mocked storages)
allow_any_instance_of(Repository).to receive(:empty?).and_return(false)
end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
index aedbaa66d34..95f7f87d37b 100644
--- a/spec/views/projects/blob/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -29,6 +29,8 @@ describe 'projects/blob/_viewer.html.haml' do
controller.params[:namespace_id] = project.namespace.to_param
controller.params[:project_id] = project.to_param
controller.params[:id] = File.join('master', blob.path)
+
+ allow(project.repository).to receive(:gitattribute).and_return(nil)
end
def render_view
diff --git a/spec/workers/cluster_platform_configure_worker_spec.rb b/spec/workers/cluster_platform_configure_worker_spec.rb
new file mode 100644
index 00000000000..1a7ad8923f6
--- /dev/null
+++ b/spec/workers/cluster_platform_configure_worker_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClusterPlatformConfigureWorker, '#execute' do
+ context 'when provider type is gcp' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+
+ it 'configures kubernetes platform' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is user' do
+ let(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ it 'configures kubernetes platform' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+
+ described_class.new.perform(123)
+ end
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 8054ec11a48..0a2dfef36a4 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -14,18 +14,25 @@ describe ClusterProvisionWorker do
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, provider_type: :user) }
+ let(:cluster) { create(:cluster, :provided_by_user) }
it 'does not provision a cluster' do
expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
described_class.new.perform(cluster.id)
end
+
+ it 'configures kubernetes platform' do
+ expect(ClusterPlatformConfigureWorker).to receive(:perform_async).with(cluster.id)
+
+ described_class.new.perform(cluster.id)
+ end
end
context 'when cluster does not exist' do
it 'does not provision a cluster' do
expect_any_instance_of(Clusters::Gcp::ProvisionService).not_to receive(:execute)
+ expect(ClusterPlatformConfigureWorker).not_to receive(:perform_async)
described_class.new.perform(123)
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index cd6661f09a1..9176eb12b12 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -6,7 +6,7 @@ describe PostReceive do
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
let(:gl_repository) { "project-#{project.id}" }
let(:key) { create(:key, user: project.owner) }
- let(:key_id) { key.shell_id }
+ let!(:key_id) { key.shell_id }
let(:project) do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
@@ -31,85 +31,108 @@ describe PostReceive do
end
describe "#process_project_changes" do
- before do
- allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ context 'empty changes' do
+ it "does not call any PushService but runs after project hooks" do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+ expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
+
+ described_class.new.perform(gl_repository, key_id, "")
+ end
end
- context "branches" do
- let(:changes) { "123456 789012 refs/heads/tést" }
+ context 'unidentified user' do
+ let!(:key_id) { "" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it 'returns false' do
+ expect(GitPushService).not_to receive(:new)
+ expect(GitTagPushService).not_to receive(:new)
+
+ expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false
end
end
- context "tags" do
- let(:changes) { "123456 789012 refs/tags/tag" }
+ context 'with changes' do
+ before do
+ allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ end
+
+ context "branches" do
+ let(:changes) { "123456 789012 refs/heads/tést" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitPushService" do
+ expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "merge-requests" do
- let(:changes) { "123456 789012 refs/merge-requests/123" }
+ context "tags" do
+ let(:changes) { "123456 789012 refs/tags/tag" }
- it "does not call any of the services" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ it "calls GitTagPushService" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
- end
- context "gitlab-ci.yml" do
- let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
+ context "merge-requests" do
+ let(:changes) { "123456 789012 refs/merge-requests/123" }
- subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
+ it "does not call any of the services" do
+ expect_any_instance_of(GitPushService).not_to receive(:execute)
+ expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+ end
- context "creates a Ci::Pipeline for every change" do
- before do
- stub_ci_pipeline_to_return_yaml_file
+ context "gitlab-ci.yml" do
+ let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
- allow_any_instance_of(Project)
- .to receive(:commit)
- .and_return(project.commit)
+ subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
- allow_any_instance_of(Repository)
- .to receive(:branch_exists?)
- .and_return(true)
- end
+ context "creates a Ci::Pipeline for every change" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
- it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
- end
+ allow_any_instance_of(Project)
+ .to receive(:commit)
+ .and_return(project.commit)
- context "does not create a Ci::Pipeline" do
- before do
- stub_ci_pipeline_yaml_file(nil)
+ allow_any_instance_of(Repository)
+ .to receive(:branch_exists?)
+ .and_return(true)
+ end
+
+ it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
end
- it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ context "does not create a Ci::Pipeline" do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ end
+
+ it { expect { subject }.not_to change { Ci::Pipeline.count } }
+ end
end
- end
- context 'after project changes hooks' do
- let(:changes) { '123456 789012 refs/heads/tést' }
- let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
+ context 'after project changes hooks' do
+ let(:changes) { '123456 789012 refs/heads/tést' }
+ let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
- before do
- allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
- # silence hooks so we can isolate
- allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- end
+ before do
+ allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
+ # silence hooks so we can isolate
+ allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
+ allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ end
- it 'calls SystemHooksService' do
- expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
+ it 'calls SystemHooksService' do
+ expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 049ffcc3407..24136a7aca5 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -16,7 +16,7 @@ singleuser:
lifecycleHooks:
postStart:
exec:
- command: ["git", "clone", "https://gitlab.com/gitlab-org/nurtch-demo.git", "DevOps-Runbook-Demo"]
+ command: ["sh", "-c", "git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true"]
ingress:
enabled: true
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index 9083fe076c3..5a7f7c0ebd1 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -67,7 +67,7 @@
@babel/template,7.1.2,MIT
@babel/traverse,7.1.0,MIT
@babel/types,7.1.2,MIT
-@gitlab-org/gitlab-svgs,1.31.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
diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
index c432be72163..02ec3e2d9fe 100644
--- a/vendor/prometheus/values.yaml
+++ b/vendor/prometheus/values.yaml
@@ -1,5 +1,7 @@
alertmanager:
enabled: false
+ image:
+ tag: v0.15.2
kubeStateMetrics:
enabled: true
@@ -16,7 +18,7 @@ rbac:
server:
fullnameOverride: "prometheus-prometheus-server"
image:
- tag: v2.1.0
+ tag: v2.4.3
serverFiles:
alerts: {}
diff --git a/yarn.lock b/yarn.lock
index 5da401c1d43..38e0f9d6201 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -621,15 +621,10 @@
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3"
integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA==
-"@gitlab-org/gitlab-svgs@^1.33.0":
- version "1.33.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a"
- integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA==
-
-"@gitlab-org/gitlab-ui@^1.8.0":
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee"
- integrity sha512-Owm8bkP4vEihiLD3pmMw1r+UWr3WYGaGUtj0JcwaAg3d05ZneozFEZjazIOWeYTcFsk+ZvNmSk1UA+ARIauhgQ==
+"@gitlab-org/gitlab-ui@^1.10.0":
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.10.0.tgz#3ac54ecaa25ea558324f0b382c97fcf9e3c4f0a5"
+ integrity sha512-kfoCKA+AmWZ3hf1wOS8W9mPJs/7lF+a01PK//+sw2MOLv6PlduJJmdN8drFuJ65o6cTJ1f9FMVB80R6D71XVKQ==
dependencies:
"@gitlab-org/gitlab-svgs" "^1.23.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -648,6 +643,11 @@
eslint-plugin-promise "^4.0.1"
eslint-plugin-vue "^5.0.0-beta.3"
+"@gitlab/svgs@^1.35.0":
+ version "1.35.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.35.0.tgz#01b6a0948bb3897fbbac9f50ce23c559c514ea0e"
+ integrity sha512-XKrTniSYKG5U8+8ZqDJqoW8ORahuPBfHrfsC1dHBPvo1xA/QGJxlpUdeqSFw2O19h481ut4yW1dF+OFpIa/mrw==
+
"@sindresorhus/is@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
@@ -5502,12 +5502,7 @@ mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
dependencies:
minimist "0.0.8"
-moment@2.x:
- version "2.19.2"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
- integrity sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA==
-
-moment@^2.21.0:
+moment@2.x, moment@^2.21.0:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=