summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Coding style proposal.md (renamed from .gitlab/issue_templates/Add style proposal.md)0
-rw-r--r--.gitlab/issue_templates/Feature proposal.md25
-rw-r--r--.gitlab/issue_templates/Security Release.md24
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md13
-rw-r--r--.gitlab/merge_request_templates/Security Release.md28
-rw-r--r--.rubocop_todo.yml5
-rw-r--r--CHANGELOG.md68
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock10
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js400
-rw-r--r--app/assets/javascripts/behaviors/markdown/editor_extensions.js106
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/bold.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/code.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_diff.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_html.js46
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/italic.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/link.js21
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/math.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/strike.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/blockquote.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/code_block.js99
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_details.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/description_term.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/details.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/doc.js15
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/emoji.js41
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/hard_break.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/heading.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/image.js52
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/list_item.js11
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js10
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/paragraph.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/reference.js52
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/summary.js27
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table.js25
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_body.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_cell.js35
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_head.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js43
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js33
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_row.js38
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js49
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/text.js20
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/video.js54
-rw-r--r--app/assets/javascripts/behaviors/markdown/schema.js24
-rw-r--r--app/assets/javascripts/behaviors/markdown/serializer.js24
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js11
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue49
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue92
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue53
-rw-r--r--app/assets/javascripts/diffs/constants.js2
-rw-r--r--app/assets/javascripts/diffs/index.js15
-rw-r--r--app/assets/javascripts/diffs/store/actions.js20
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js6
-rw-r--r--app/assets/javascripts/diffs/store/utils.js5
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js4
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js6
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js22
-rw-r--r--app/assets/javascripts/groups_select.js172
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue13
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js6
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js12
-rw-r--r--app/assets/javascripts/issuable_context.js12
-rw-r--r--app/assets/javascripts/issuable_form.js62
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue5
-rw-r--r--app/assets/javascripts/label_manager.js13
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js26
-rw-r--r--app/assets/javascripts/main.js30
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue28
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue28
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue45
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue33
-rw-r--r--app/assets/javascripts/notes/stores/actions.js11
-rw-r--r--app/assets/javascripts/pages/explore/projects/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/tags/releases/index.js8
-rw-r--r--app/assets/javascripts/project_select.js174
-rw-r--r--app/assets/javascripts/project_select_combo_button.js10
-rw-r--r--app/assets/javascripts/users_select.js190
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue8
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss12
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/highlight/none.scss28
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss5
-rw-r--r--app/assets/stylesheets/pages/diff.scss10
-rw-r--r--app/assets/stylesheets/pages/groups.scss108
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss153
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb (renamed from app/controllers/concerns/issues_action.rb)33
-rw-r--r--app/controllers/concerns/membership_actions.rb4
-rw-r--r--app/controllers/concerns/merge_requests_action.rb25
-rw-r--r--app/controllers/dashboard/milestones_controller.rb2
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/explore/projects_controller.rb6
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/import/bitbucket_controller.rb4
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb7
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/notification_settings_controller.rb7
-rw-r--r--app/controllers/projects/environments_controller.rb21
-rw-r--r--app/controllers/projects/issues_controller.rb14
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb9
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb7
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb7
-rw-r--r--app/finders/contributed_projects_finder.rb7
-rw-r--r--app/finders/milestones_finder.rb9
-rw-r--r--app/helpers/emails_helper.rb8
-rw-r--r--app/helpers/external_wiki_helper.rb12
-rw-r--r--app/helpers/import_helper.rb4
-rw-r--r--app/helpers/members_helper.rb15
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb16
-rw-r--r--app/helpers/release_blog_post_helper.rb7
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/ci/bridge.rb21
-rw-r--r--app/models/ci/build.rb56
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb1
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/commit.rb5
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/ci/metadatable.rb69
-rw-r--r--app/models/concerns/ci/processable.rb27
-rw-r--r--app/models/container_repository.rb11
-rw-r--r--app/models/dashboard_group_milestone.rb7
-rw-r--r--app/models/global_milestone.rb1
-rw-r--r--app/models/group_milestone.rb3
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/internal_id.rb17
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/lfs_download_object.rb22
-rw-r--r--app/models/member.rb3
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/milestone.rb13
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/project.rb46
-rw-r--r--app/models/project_feature.rb19
-rw-r--r--app/models/project_services/bamboo_service.rb32
-rw-r--r--app/models/project_team.rb12
-rw-r--r--app/models/user.rb10
-rw-r--r--app/policies/ci/pipeline_policy.rb9
-rw-r--r--app/policies/container_repository_policy.rb5
-rw-r--r--app/policies/issue_policy.rb1
-rw-r--r--app/policies/note_policy.rb1
-rw-r--r--app/policies/personal_snippet_policy.rb5
-rw-r--r--app/policies/project_policy.rb24
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/presenters/ci/trigger_presenter.rb19
-rw-r--r--app/presenters/commit_presenter.rb13
-rw-r--r--app/presenters/merge_request_presenter.rb4
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/container_repository_entity.rb2
-rw-r--r--app/serializers/container_tag_entity.rb2
-rw-r--r--app/serializers/error_tracking/project_entity.rb7
-rw-r--r--app/serializers/error_tracking/project_serializer.rb7
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/services/ci/process_pipeline_service.rb16
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb13
-rw-r--r--app/services/members/destroy_service.rb28
-rw-r--r--app/services/notes/build_service.rb15
-rw-r--r--app/services/notification_service.rb3
-rw-r--r--app/services/projects/after_rename_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb94
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb57
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb26
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb46
-rw-r--r--app/services/projects/hashed_storage/migration_service.rb39
-rw-r--r--app/services/projects/hashed_storage_migration_service.rb27
-rw-r--r--app/services/projects/import_error_filter.rb14
-rw-r--r--app/services/projects/import_service.rb20
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb13
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb109
-rw-r--r--app/services/projects/update_pages_service.rb41
-rw-r--r--app/services/protected_branches/api_service.rb8
-rw-r--r--app/services/suggestions/apply_service.rb15
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml1
-rw-r--r--app/views/admin/runners/_runner.html.haml4
-rw-r--r--app/views/admin/runners/index.html.haml4
-rw-r--r--app/views/clusters/clusters/_form.html.haml (renamed from app/views/clusters/clusters/_integration_form.html.haml)0
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml50
-rw-r--r--app/views/clusters/clusters/index.html.haml8
-rw-r--r--app/views/clusters/clusters/new.html.haml4
-rw-r--r--app/views/clusters/clusters/show.html.haml15
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml39
-rw-r--r--app/views/clusters/platforms/kubernetes/_form.html.haml58
-rw-r--r--app/views/dashboard/milestones/index.html.haml2
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml71
-rw-r--r--app/views/groups/milestones/_form.html.haml14
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/show.html.haml95
-rw-r--r--app/views/ide/_show.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml25
-rw-r--r--app/views/notify/_note_email.text.erb4
-rw-r--r--app/views/notify/autodevops_disabled_email.text.erb2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml6
-rw-r--r--app/views/notify/issue_status_changed_email.html.haml2
-rw-r--r--app/views/notify/issue_status_changed_email.text.erb2
-rw-r--r--app/views/notify/member_access_requested_email.text.erb2
-rw-r--r--app/views/notify/member_invite_accepted_email.text.erb2
-rw-r--r--app/views/notify/member_invited_email.text.erb2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml4
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml4
-rw-r--r--app/views/notify/new_gpg_key_email.html.haml2
-rw-r--r--app/views/notify/new_gpg_key_email.text.erb2
-rw-r--r--app/views/notify/new_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb4
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb4
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.html.haml2
-rw-r--r--app/views/notify/new_ssh_key_email.text.erb2
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/notify/new_user_email.text.erb2
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb6
-rw-r--r--app/views/notify/pipeline_success_email.text.erb6
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml2
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.html.haml2
-rw-r--r--app/views/notify/reassigned_issue_email.text.erb2
-rw-r--r--app/views/notify/reassigned_merge_request_email.html.haml4
-rw-r--r--app/views/notify/reassigned_merge_request_email.text.erb4
-rw-r--r--app/views/notify/resolved_all_discussions_email.html.haml2
-rw-r--r--app/views/notify/resolved_all_discussions_email.text.erb2
-rw-r--r--app/views/projects/_home_panel.html.haml24
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml22
-rw-r--r--app/views/projects/ci/lints/_create.html.haml26
-rw-r--r--app/views/projects/ci/lints/show.html.haml12
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/show.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml5
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml3
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml5
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml20
-rw-r--r--app/views/projects/milestones/edit.html.haml10
-rw-r--r--app/views/projects/milestones/index.html.haml9
-rw-r--r--app/views/projects/milestones/new.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml28
-rw-r--r--app/views/projects/pages_domains/_form.html.haml13
-rw-r--r--app/views/projects/pages_domains/edit.html.haml4
-rw-r--r--app/views/projects/pages_domains/new.html.haml8
-rw-r--r--app/views/projects/pages_domains/show.html.haml27
-rw-r--r--app/views/projects/pipelines/_info.html.haml33
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml3
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml10
-rw-r--r--app/views/projects/wikis/pages.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml20
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml20
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml6
-rw-r--r--app/views/shared/milestones/_search_form.html.haml8
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/notifications/_new_button.html.haml (renamed from app/views/projects/buttons/_notifications.html.haml)8
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/workers/all_queues.yml7
-rw-r--r--app/workers/cleanup_container_repository_worker.rb53
-rw-r--r--app/workers/delete_container_repository_worker.rb2
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb16
-rw-r--r--app/workers/project_migrate_hashed_storage_worker.rb21
-rw-r--r--app/workers/storage_migrator_worker.rb10
-rwxr-xr-xbin/secpick4
-rw-r--r--changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml5
-rw-r--r--changelogs/unreleased/24875-label.yml5
-rw-r--r--changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml5
-rw-r--r--changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml5
-rw-r--r--changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml5
-rw-r--r--changelogs/unreleased/44698-recaptcha.yml5
-rw-r--r--changelogs/unreleased/45791-number-of-repositories-usage-ping.yml5
-rw-r--r--changelogs/unreleased/50352-sort-save.yml5
-rw-r--r--changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml5
-rw-r--r--changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml5
-rw-r--r--changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml5
-rw-r--r--changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml5
-rw-r--r--changelogs/unreleased/54905-milestone-search.yml5
-rw-r--r--changelogs/unreleased/55820-adds-common-name-chart-value.yml5
-rw-r--r--changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml5
-rw-r--r--changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml5
-rw-r--r--changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml5
-rw-r--r--changelogs/unreleased/ab-54270-github-iid.yml5
-rw-r--r--changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml5
-rw-r--r--changelogs/unreleased/an-opentracing-active-record-tracing.yml5
-rw-r--r--changelogs/unreleased/an-opentracing-render-tracing.yml5
-rw-r--r--changelogs/unreleased/cluster_status_for_ugprading.yml5
-rw-r--r--changelogs/unreleased/container-repository-cleanup-api.yml5
-rw-r--r--changelogs/unreleased/dm-copy-suggestion-as-gfm.yml5
-rw-r--r--changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml5
-rw-r--r--changelogs/unreleased/fix-49388.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-clusters.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-projects-ci.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml5
-rw-r--r--changelogs/unreleased/hnk-master-patch-61932.yml5
-rw-r--r--changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml5
-rw-r--r--changelogs/unreleased/patch-38.yml5
-rw-r--r--changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml5
-rw-r--r--changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml5
-rw-r--r--changelogs/unreleased/remove-diff-coloring.yml5
-rw-r--r--changelogs/unreleased/security-22076-sanitize-url-in-names.yml6
-rw-r--r--changelogs/unreleased/security-55320-stored-xss-in-user-status.yml5
-rw-r--r--changelogs/unreleased/security-stored-xss-via-katex.yml5
-rw-r--r--changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml5
-rw-r--r--changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml5
-rw-r--r--changelogs/unreleased/sh-fix-pages-zip-constant.yml5
-rw-r--r--changelogs/unreleased/sh-issue-53419-fix.yml5
-rw-r--r--changelogs/unreleased/test-permissions.yml5
-rw-r--r--changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml5
-rw-r--r--config/initializers/kubeclient.rb22
-rw-r--r--config/initializers/tracing.rb4
-rw-r--r--config/routes/import.rb9
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--config/webpack.config.js3
-rw-r--r--db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb21
-rw-r--r--db/post_migrate/20181219130552_update_project_import_visibility_level.rb60
-rw-r--r--db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb43
-rw-r--r--db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb15
-rw-r--r--db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb18
-rw-r--r--db/schema.rb4
-rw-r--r--doc/README.md5
-rw-r--r--doc/administration/git_protocol.md7
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/pages/index.md36
-rw-r--r--doc/api/README.md5
-rw-r--r--doc/api/container_registry.md200
-rw-r--r--doc/api/issues.md18
-rw-r--r--doc/api/project_clusters.md23
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/ci/caching/index.md12
-rw-r--r--doc/ci/docker/using_kaniko.md2
-rw-r--r--doc/ci/environments.md2
-rw-r--r--doc/ci/examples/artifactory_and_gitlab/index.md2
-rw-r--r--doc/ci/examples/container_scanning.md4
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md2
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md2
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md2
-rw-r--r--doc/ci/interactive_web_terminal/index.md11
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md85
-rw-r--r--doc/ci/variables/img/variables.pngbin34838 -> 0 bytes
-rw-r--r--doc/ci/yaml/README.md8
-rw-r--r--doc/development/documentation/styleguide.md33
-rw-r--r--doc/development/ee_features.md14
-rw-r--r--doc/development/i18n/externalization.md10
-rw-r--r--doc/install/installation.md130
-rw-r--r--doc/integration/bitbucket.md6
-rw-r--r--doc/integration/github.md6
-rw-r--r--doc/policy/maintenance.md36
-rw-r--r--doc/topics/autodevops/index.md9
-rw-r--r--doc/topics/autodevops/quick_start_guide.md4
-rw-r--r--doc/user/permissions.md7
-rw-r--r--doc/user/project/import/index.md7
-rw-r--r--doc/user/project/integrations/prometheus.md4
-rw-r--r--doc/user/project/integrations/prometheus_library/index.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md2
-rw-r--r--doc/user/project/merge_requests/index.md12
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/pages/introduction.md4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/container_registry.rb143
-rw-r--r--lib/api/entities.rb5
-rw-r--r--lib/api/entities/container_registry.rb29
-rw-r--r--lib/api/helpers/presentable.rb29
-rw-r--r--lib/api/pipelines.rb6
-rw-r--r--lib/api/triggers.rb10
-rw-r--r--lib/banzai/filter/autolink_filter.rb13
-rw-r--r--lib/banzai/filter/emoji_filter.rb1
-rw-r--r--lib/banzai/filter/external_link_filter.rb85
-rw-r--r--lib/banzai/filter/image_lazy_load_filter.rb1
-rw-r--r--lib/banzai/filter/image_link_filter.rb1
-rw-r--r--lib/banzai/filter/inline_diff_filter.rb1
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb15
-rw-r--r--lib/banzai/filter/markdown_engines/redcarpet.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb2
-rw-r--r--lib/banzai/filter/math_filter.rb3
-rw-r--r--lib/banzai/filter/mermaid_filter.rb1
-rw-r--r--lib/banzai/filter/reference_filter.rb1
-rw-r--r--lib/banzai/filter/sanitization_filter.rb3
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb3
-rw-r--r--lib/banzai/filter/suggestion_filter.rb1
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb1
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb1
-rw-r--r--lib/banzai/filter/task_list_filter.rb4
-rw-r--r--lib/banzai/filter/video_link_filter.rb1
-rw-r--r--lib/banzai/pipeline/atom_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/broadcast_message_pipeline.rb6
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb4
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb10
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb6
-rw-r--r--lib/container_registry/tag.rb38
-rw-r--r--lib/gitlab.rb48
-rw-r--r--lib/gitlab/ci/ansi2html.rb18
-rw-r--r--lib/gitlab/ci/config/entry/job.rb2
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb6
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb10
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb8
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb1
-rw-r--r--lib/gitlab/ci/status/external/common.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/trace/stream.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb7
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb2
-rw-r--r--lib/gitlab/error_tracking/project.rb16
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb6
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb4
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb6
-rw-r--r--lib/gitlab/github_import/importer/lfs_object_importer.rb8
-rw-r--r--lib/gitlab/github_import/importer/milestones_importer.rb12
-rw-r--r--lib/gitlab/github_import/representation/lfs_object.rb4
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb24
-rw-r--r--lib/gitlab/import/merge_request_helpers.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb9
-rw-r--r--lib/gitlab/import_export/shared.rb39
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb5
-rw-r--r--lib/gitlab/metrics/influx_db.rb4
-rw-r--r--lib/gitlab/path_regex.rb3
-rw-r--r--lib/gitlab/release_blog_post.rb40
-rw-r--r--lib/gitlab/sql/union.rb2
-rw-r--r--lib/gitlab/tracing/common.rb12
-rw-r--r--lib/gitlab/tracing/rails/action_view_subscriber.rb75
-rw-r--r--lib/gitlab/tracing/rails/active_record_subscriber.rb49
-rw-r--r--lib/gitlab/tracing/rails/rails_common.rb24
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/version_info.rb8
-rw-r--r--lib/safe_zip/entry.rb97
-rw-r--r--lib/safe_zip/extract.rb73
-rw-r--r--lib/safe_zip/extract_params.rb36
-rw-r--r--lib/sentry/client.rb58
-rw-r--r--lib/tasks/gitlab/storage.rake2
-rw-r--r--locale/gitlab.pot198
-rw-r--r--package.json19
-rw-r--r--qa/Gemfile.lock2
-rw-r--r--qa/Rakefile6
-rw-r--r--qa/qa.rb5
-rw-r--r--qa/qa/git/repository.rb46
-rw-r--r--qa/qa/page/base.rb4
-rw-r--r--qa/qa/page/component/lazy_loader.rb15
-rw-r--r--qa/qa/page/group/show.rb2
-rw-r--r--qa/qa/page/label/index.rb8
-rw-r--r--qa/qa/page/main/menu.rb8
-rw-r--r--qa/qa/page/merge_request/show.rb28
-rw-r--r--qa/qa/page/project/branches/show.rb4
-rw-r--r--qa/qa/page/project/menu.rb136
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb2
-rw-r--r--qa/qa/page/project/sub_menus/ci_cd.rb25
-rw-r--r--qa/qa/page/project/sub_menus/issues.rb52
-rw-r--r--qa/qa/page/project/sub_menus/operations.rb47
-rw-r--r--qa/qa/page/project/sub_menus/settings.rb61
-rw-r--r--qa/qa/page/project/wiki/new.rb40
-rw-r--r--qa/qa/resource/repository/push.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb4
-rw-r--r--qa/qa/support/api.rb18
-rw-r--r--qa/qa/support/page/logging.rb11
-rw-r--r--qa/qa/tools/delete_subgroups.rb66
-rw-r--r--qa/spec/git/repository_spec.rb134
-rw-r--r--qa/spec/page/logging_spec.rb12
-rw-r--r--qa/spec/support/stub_env.rb2
-rwxr-xr-xscripts/trigger-build1
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb45
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb18
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb31
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb11
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb22
-rw-r--r--spec/controllers/import/github_controller_spec.rb8
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb43
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb12
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb11
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb47
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb2
-rw-r--r--spec/controllers/users_controller_spec.rb32
-rw-r--r--spec/factories/ci/bridge.rb12
-rw-r--r--spec/factories/container_repositories.rb2
-rw-r--r--spec/factories/error_tracking/project.rb15
-rw-r--r--spec/features/boards/modal_filter_spec.rb6
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb4
-rw-r--r--spec/features/dashboard/help_spec.rb8
-rw-r--r--spec/features/dashboard/projects_spec.rb21
-rw-r--r--spec/features/groups/group_settings_spec.rb8
-rw-r--r--spec/features/groups_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb10
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb10
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb6
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb246
-rw-r--r--spec/features/markdown/math_spec.rb18
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb1
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb1
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb12
-rw-r--r--spec/features/merge_request/user_toggles_whitespace_changes_spec.rb12
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb4
-rw-r--r--spec/features/projects/deploy_keys_spec.rb2
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb43
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb10
-rw-r--r--spec/features/projects/settings/user_changes_default_branch_spec.rb3
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb2
-rw-r--r--spec/features/projects/tags/user_edits_tags_spec.rb82
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/security/project/internal_access_spec.rb6
-rw-r--r--spec/features/security/project/private_access_spec.rb2
-rw-r--r--spec/features/security/project/public_access_spec.rb10
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb12
-rw-r--r--spec/finders/merge_requests_finder_spec.rb32
-rw-r--r--spec/finders/milestones_finder_spec.rb6
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json1
-rw-r--r--spec/fixtures/api/schemas/error_tracking/list_projects.json13
-rw-r--r--spec/fixtures/api/schemas/error_tracking/project.json19
-rw-r--r--spec/fixtures/api/schemas/registry/repository.json9
-rw-r--r--spec/fixtures/api/schemas/registry/tag.json7
-rw-r--r--spec/fixtures/pages_non_writeable.zipbin0 -> 727 bytes
-rw-r--r--spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zipbin0 -> 1183 bytes
-rw-r--r--spec/fixtures/safe_zip/invalid-symlinks-outside.zipbin0 -> 1309 bytes
-rw-r--r--spec/fixtures/safe_zip/valid-non-writeable.zipbin0 -> 727 bytes
-rw-r--r--spec/fixtures/safe_zip/valid-simple.zipbin0 -> 1144 bytes
-rw-r--r--spec/fixtures/safe_zip/valid-symlinks-first.zipbin0 -> 528 bytes
-rw-r--r--spec/fixtures/sentry/list_projects_sample_response.json81
-rw-r--r--spec/helpers/emails_helper_spec.rb14
-rw-r--r--spec/helpers/import_helper_spec.rb4
-rw-r--r--spec/helpers/issuables_helper_spec.rb1
-rw-r--r--spec/helpers/members_helper_spec.rb4
-rw-r--r--spec/helpers/notes_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb36
-rw-r--r--spec/helpers/submodule_helper_spec.rb2
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js4
-rw-r--r--spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js33
-rw-r--r--spec/javascripts/diffs/components/settings_dropdown_spec.js167
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js52
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js53
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js24
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js52
-rw-r--r--spec/javascripts/ide/components/ide_status_bar_spec.js3
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js4
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js152
-rw-r--r--spec/javascripts/monitoring/mock_data.js80
-rw-r--r--spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js33
-rw-r--r--spec/javascripts/notes/components/discussion_resolve_button_spec.js74
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js52
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js142
-rw-r--r--spec/javascripts/test_bundle.js10
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js6
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb65
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb46
-rw-r--r--spec/lib/banzai/filter/project_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb7
-rw-r--r--spec/lib/banzai/pipeline/description_pipeline_spec.rb4
-rw-r--r--spec/lib/banzai/pipeline/email_pipeline_spec.rb14
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/status/external/common_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb33
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb37
-rw-r--r--spec/lib/gitlab/git_access_spec.rb23
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb9
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb65
-rw-r--r--spec/lib/gitlab/import_export/shared_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb32
-rw-r--r--spec/lib/gitlab/release_blog_post_spec.rb97
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb147
-rw-r--r--spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb73
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb4
-rw-r--r--spec/lib/gitlab_spec.rb76
-rw-r--r--spec/lib/safe_zip/entry_spec.rb196
-rw-r--r--spec/lib/safe_zip/extract_params_spec.rb54
-rw-r--r--spec/lib/safe_zip/extract_spec.rb80
-rw-r--r--spec/lib/sentry/client_spec.rb159
-rw-r--r--spec/mailers/notify_spec.rb8
-rw-r--r--spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb47
-rw-r--r--spec/migrations/update_project_import_visibility_level_spec.rb86
-rw-r--r--spec/models/ability_spec.rb1
-rw-r--r--spec/models/application_record_spec.rb13
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb23
-rw-r--r--spec/models/commit_spec.rb1
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb8
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/redactable_spec.rb4
-rw-r--r--spec/models/global_milestone_spec.rb6
-rw-r--r--spec/models/identity_spec.rb34
-rw-r--r--spec/models/internal_id_spec.rb23
-rw-r--r--spec/models/lfs_download_object_spec.rb68
-rw-r--r--spec/models/milestone_spec.rb23
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb26
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb21
-rw-r--r--spec/models/project_spec.rb104
-rw-r--r--spec/models/project_team_spec.rb15
-rw-r--r--spec/models/sent_notification_spec.rb2
-rw-r--r--spec/models/user_preference_spec.rb25
-rw-r--r--spec/models/user_spec.rb27
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb8
-rw-r--r--spec/policies/note_policy_spec.rb2
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb29
-rw-r--r--spec/policies/project_policy_spec.rb80
-rw-r--r--spec/policies/project_snippet_policy_spec.rb20
-rw-r--r--spec/presenters/ci/trigger_presenter_spec.rb51
-rw-r--r--spec/presenters/commit_presenter_spec.rb54
-rw-r--r--spec/requests/api/container_registry_spec.rb224
-rw-r--r--spec/requests/api/markdown_spec.rb2
-rw-r--r--spec/requests/api/triggers_spec.rb14
-rw-r--r--spec/requests/lfs_http_spec.rb23
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb6
-rw-r--r--spec/serializers/container_tag_entity_spec.rb2
-rw-r--r--spec/serializers/group_child_entity_spec.rb1
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb39
-rw-r--r--spec/services/members/destroy_service_spec.rb60
-rw-r--r--spec/services/notes/build_service_spec.rb9
-rw-r--r--spec/services/notes/create_service_spec.rb4
-rw-r--r--spec/services/notification_service_spec.rb29
-rw-r--r--spec/services/projects/after_rename_service_spec.rb4
-rw-r--r--spec/services/projects/container_repository/cleanup_tags_service_spec.rb162
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb34
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb32
-rw-r--r--spec/services/projects/hashed_storage/migration_service_spec.rb (renamed from spec/services/projects/hashed_storage_migration_service_spec.rb)2
-rw-r--r--spec/services/projects/import_error_filter_spec.rb17
-rw-r--r--spec/services/projects/import_service_spec.rb13
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb18
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb162
-rw-r--r--spec/services/projects/update_pages_service_spec.rb35
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb4
-rw-r--r--spec/services/suggestions/apply_service_spec.rb7
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb2
-rw-r--r--spec/support/helpers/rake_helpers.rb2
-rw-r--r--spec/support/helpers/select2_helper.rb6
-rw-r--r--spec/support/helpers/stub_env.rb2
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb34
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb10
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb10
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb54
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb6
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb4
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb51
-rw-r--r--spec/workers/cleanup_container_repository_worker_spec.rb47
-rw-r--r--spec/workers/hashed_storage/migrator_worker_spec.rb (renamed from spec/workers/storage_migrator_worker_spec.rb)4
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb23
-rw-r--r--yarn.lock431
724 files changed, 11259 insertions, 3829 deletions
diff --git a/.gitlab/issue_templates/Add style proposal.md b/.gitlab/issue_templates/Coding style proposal.md
index 1a3be44bea0..1a3be44bea0 100644
--- a/.gitlab/issue_templates/Add style proposal.md
+++ b/.gitlab/issue_templates/Coding style proposal.md
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index 4d4d3bfda15..0b22c7bc26b 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -4,7 +4,30 @@
### Target audience
-<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst". Use the persona labels as well https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A -->
+<!--- For whom are we doing this? Include a [persona](https://design.gitlab.com/research/personas)
+listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A),
+or define a specific company role, e.g. "Release Manager".
+
+Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply)
+
+- Parker, Product Manager, https://design.gitlab.com/research/personas#persona-parker
+/label ~"Persona: Product Manager"
+
+- Delaney, Development Team Lead, https://design.gitlab.com/research/personas#persona-delaney
+/label ~"Persona: Development Team Lead"
+
+- Sasha, Software Developer, https://design.gitlab.com/research/personas#persona-sasha
+/label ~"Persona: Software developer"
+
+- Devon, DevOps Engineer, https://design.gitlab.com/research/personas#persona-devon
+/label ~"Persona: DevOps Engineer"
+
+- Sidney, Systems Administrator, https://design.gitlab.com/research/personas#persona-sidney
+/label ~"Persona: Systems Administrator"
+
+- Sam, Security Analyst, https://design.gitlab.com/research/personas#persona-sam
+/label ~"Persona: Security Analyst"
+-->
### Further details
diff --git a/.gitlab/issue_templates/Security Release.md b/.gitlab/issue_templates/Security Release.md
index 1734e915ad2..ae469d3b125 100644
--- a/.gitlab/issue_templates/Security Release.md
+++ b/.gitlab/issue_templates/Security Release.md
@@ -32,12 +32,12 @@ Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X`
- {https://dev.gitlab.org/gitlab/gitlabhq/issues link}
-| Version | MR | Status|
-|---------|----|-------|
-| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | |
-| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | |
-| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | |
-| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | |
+| Version | MR |
+|---------|----|
+| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
+| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
+| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
+| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} |
@@ -46,12 +46,12 @@ Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X`
* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link}
-| Version | MR | Status|
-|---------|----|-------|
-| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | |
-| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | |
-| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | |
-| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | |
+| Version | MR |
+|---------|----|
+| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
+| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
+| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
+| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} |
## QA
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index f9bf700f809..4bc4215d21b 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -3,20 +3,17 @@
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
-Set the title to: `[Security] Description of the original issue`
+Set the title to: `Description of the original issue`
-->
-### Prior to the security release
+### Prior to starting the security release work
- [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
-- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
-- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
-- [ ] Add a link to the MR to the [links section](#links)
-- [ ] Add a link to an EE MR if required
-- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
-- [ ] Add a link to this issue on the original security issue.
+- [ ] Create a new branch prefixing it with `security-`
+- [ ] Create a MR targeting `dev.gitlab.org` `master`
+- [ ] Add a link to this issue in the original security issue on `gitlab.com`.
#### Backports
diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md
new file mode 100644
index 00000000000..d72b4eb1cb6
--- /dev/null
+++ b/.gitlab/merge_request_templates/Security Release.md
@@ -0,0 +1,28 @@
+<!--
+# README first!
+This MR should be created on `dev.gitlab.org`.
+
+See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md).
+
+-->
+## Related issues
+
+<!-- Mention the issue(s) this MR is related to -->
+
+## Author's checklist
+
+- [ ] Link to the developer security workflow issue on `dev.gitlab.org`
+- [ ] MR targets `master` or `security-X-Y` for backports
+- [ ] Milestone is set for the version this MR applies to
+- [ ] Title of this MR is the same as for all backports
+- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
+- [ ] Add a link to this MR in the `links` section of related issue
+- [ ] Add a link to an EE MR if required
+- [ ] Assign to a reviewer
+
+## Reviewers checklist
+
+- [ ] Correct milestone is applied and the title is matching across all backports
+- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
+
+/label ~security ~"Merge into Security"
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 91810d84c50..c42d11a860e 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -437,11 +437,6 @@ Style/LineEndConcatenation:
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/incoming_email_spec.rb'
-# Offense count: 39
-# Cop supports --auto-correct.
-Style/MethodCallWithoutArgsParentheses:
- Enabled: false
-
# Offense count: 18
Style/MethodMissing:
Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1deab58d38..4985c607d57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,43 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.7.2 (2019-01-29)
+
+### Fixed (1 change)
+
+- Fix uninitialized constant with GitLab Pages.
+
+
+## 11.7.1 (2019-01-28)
+
+### Security (24 changes)
+
+- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770
+- Don't process MR refs for guests in the notes. !2771
+- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2828
+- Fixed XSS content in KaTex links.
+- Disallows unauthorized users from accessing the pipelines section.
+- Verify that LFS upload requests are genuine.
+- Extract GitLab Pages using RubyZip.
+- Prevent awarding emojis to notes whose parent is not visible to user.
+- Prevent unauthorized replies when discussion is locked or confidential.
+- Disable git v2 protocol temporarily.
+- Fix showing ci status for guest users when public pipline are not set.
+- Fix contributed projects info still visible when user enable private profile.
+- Add subresources removal to member destroy service.
+- Add more LFS validations to prevent forgery.
+- Use common error for unauthenticated users when creating issues.
+- Fix slow regex in project reference pattern.
+- Fix private user email being visible in push (and tag push) webhooks.
+- Fix wiki access rights when external wiki is enabled.
+- Group guests are no longer able to see merge requests they don't have access to at group level.
+- Fix path disclosure on project import error.
+- Restrict project import visibility based on its group.
+- Expose CI/CD trigger token only to the trigger owner.
+- Notify only users who can access the project on project move.
+- Alias GitHub and BitBucket OAuth2 callback URLs.
+
+
## 11.7.0 (2019-01-22)
### Security (14 changes, 1 of them is from the community)
@@ -188,6 +225,10 @@ entry.
- Update url placeholder for the sentry configuration page. !24338
+## 11.6.8 (2019-01-30)
+
+- No changes.
+
## 11.6.5 (2019-01-17)
### Fixed (5 changes)
@@ -528,6 +569,33 @@ entry.
- Enable Rubocop on lib/gitlab. (gfyoung)
+## 11.5.8 (2019-01-28)
+
+### Security (21 changes)
+
+- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770
+- Don't process MR refs for guests in the notes. !2771
+- Fixed XSS content in KaTex links.
+- Verify that LFS upload requests are genuine.
+- Extract GitLab Pages using RubyZip.
+- Prevent awarding emojis to notes whose parent is not visible to user.
+- Prevent unauthorized replies when discussion is locked or confidential.
+- Disable git v2 protocol temporarily.
+- Fix showing ci status for guest users when public pipline are not set.
+- Fix contributed projects info still visible when user enable private profile.
+- Disallows unauthorized users from accessing the pipelines section.
+- Add more LFS validations to prevent forgery.
+- Use common error for unauthenticated users when creating issues.
+- Fix slow regex in project reference pattern.
+- Fix private user email being visible in push (and tag push) webhooks.
+- Fix wiki access rights when external wiki is enabled.
+- Fix path disclosure on project import error.
+- Restrict project import visibility based on its group.
+- Expose CI/CD trigger token only to the trigger owner.
+- Notify only users who can access the project on project move.
+- Alias GitHub and BitBucket OAuth2 callback URLs.
+
+
## 11.5.5 (2018-12-20)
### Security (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index cd99d386a8d..63e799cf451 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.14.0 \ No newline at end of file
+1.14.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index da156181014..0e79152459e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.1.0 \ No newline at end of file
+8.1.1
diff --git a/Gemfile b/Gemfile
index b3eeb3ec0ec..a5f3afcaa55 100644
--- a/Gemfile
+++ b/Gemfile
@@ -57,6 +57,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
+gem 'rubyzip', '~> 1.2.2', require: 'zip'
# Browser detection
gem 'browser', '~> 2.5'
@@ -224,7 +225,7 @@ gem 'asana', '~> 0.8.1'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
-gem 'kubeclient', '~> 4.0.0'
+gem 'kubeclient', '~> 4.2.2'
# Sanitize user input
gem 'sanitize', '~> 4.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 419a6831924..1c28176ac62 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -425,7 +425,7 @@ GEM
kgio (2.10.0)
knapsack (1.17.0)
rake
- kubeclient (4.0.0)
+ kubeclient (4.2.2)
http (~> 3.0)
recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0)
@@ -627,7 +627,7 @@ GEM
httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
- rack-protection (2.0.4)
+ rack-protection (2.0.5)
rack
rack-proxy (0.6.0)
rack
@@ -823,8 +823,9 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.2.3)
+ sidekiq (5.2.5)
connection_pool (~> 2.2, >= 2.2.2)
+ rack (>= 1.5.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
sidekiq-cron (1.0.4)
@@ -1053,7 +1054,7 @@ DEPENDENCIES
jwt (~> 2.1.0)
kaminari (~> 1.0)
knapsack (~> 1.17)
- kubeclient (~> 4.0.0)
+ kubeclient (~> 4.2.2)
letter_opener_web (~> 1.3.0)
license_finder (~> 5.4)
licensee (~> 8.9)
@@ -1137,6 +1138,7 @@ DEPENDENCIES
ruby-prof (~> 0.17.0)
ruby-progressbar
ruby_parser (~> 3.8)
+ rubyzip (~> 1.2.2)
rugged (~> 0.27)
sanitize (~> 4.6)
sass (~> 3.5)
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index fe02096d903..947d019c725 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,320 +1,8 @@
-/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */
-
import $ from 'jquery';
-import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
-import { placeholderImage } from '~/lazy_loader';
-
-const gfmRules = {
- // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
- // GitLab Flavored Markdown (GFM) to HTML.
- // These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
- // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
- // from GFM should have a handler here, in reverse order.
- // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
- InlineDiffFilter: {
- 'span.idiff.addition'(el, text) {
- return `{+${text}+}`;
- },
- 'span.idiff.deletion'(el, text) {
- return `{-${text}-}`;
- },
- },
- TaskListFilter: {
- 'input[type=checkbox].task-list-item-checkbox'(el) {
- return `[${el.checked ? 'x' : ' '}]`;
- },
- },
- ReferenceFilter: {
- '.tooltip'(el) {
- return '';
- },
- 'a.gfm:not([data-link=true])'(el, text) {
- return el.dataset.original || text;
- },
- },
- AutolinkFilter: {
- a(el, text) {
- // Fallback on the regular MarkdownFilter's `a` handler.
- if (text !== el.getAttribute('href')) return false;
-
- return text;
- },
- },
- TableOfContentsFilter: {
- 'ul.section-nav'(el) {
- return '[[_TOC_]]';
- },
- },
- EmojiFilter: {
- 'img.emoji'(el) {
- return el.getAttribute('alt');
- },
- 'gl-emoji'(el) {
- return `:${el.getAttribute('data-name')}:`;
- },
- },
- ImageLinkFilter: {
- 'a.no-attachment-icon'(el, text) {
- return text;
- },
- },
- ImageLazyLoadFilter: {
- img(el, text) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
- },
- },
- VideoLinkFilter: {
- '.video-container'(el) {
- const videoEl = el.querySelector('video');
- if (!videoEl) return false;
-
- return CopyAsGFM.nodeToGFM(videoEl);
- },
- video(el) {
- return `![${el.dataset.title}](${el.getAttribute('src')})`;
- },
- },
- MermaidFilter: {
- 'svg.mermaid'(el, text) {
- const sourceEl = el.querySelector('text.source');
- if (!sourceEl) return false;
-
- return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
- },
- 'svg.mermaid style, svg.mermaid g'(el, text) {
- // We don't want to include the content of these elements in the copied text.
- return '';
- },
- },
- MathFilter: {
- 'pre.code.math[data-math-style=display]'(el, text) {
- return `\`\`\`math\n${text.trim()}\n\`\`\``;
- },
- 'code.code.math[data-math-style=inline]'(el, text) {
- return `$\`${text}\`$`;
- },
- 'span.katex-display span.katex-mathml'(el) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
- },
- 'span.katex-mathml'(el) {
- const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
- if (!mathAnnotation) return false;
-
- return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
- },
- 'span.katex-html'(el) {
- // We don't want to include the content of this element in the copied text.
- return '';
- },
- 'annotation[encoding="application/x-tex"]'(el, text) {
- return text.trim();
- },
- },
- SanitizationFilter: {
- 'a[name]:not([href]):empty'(el) {
- return el.outerHTML;
- },
- dl(el, text) {
- let lines = text
- .replace(/\n\n/g, '\n')
- .trim()
- .split('\n');
- // Add two spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- lines = lines.map(l => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- });
-
- return `<dl>\n${lines.join('\n')}\n</dl>\n`;
- },
- 'dt, dd, summary, details'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>\n`;
- },
- 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
- const tag = el.nodeName.toLowerCase();
- return `<${tag}>${text}</${tag}>`;
- },
- },
- SyntaxHighlightFilter: {
- 'pre.code.highlight'(el, t) {
- const text = t.trimRight();
-
- let lang = el.getAttribute('lang');
- if (!lang || lang === 'plaintext') {
- lang = '';
- }
-
- // Prefixes lines with 4 spaces if the code contains triple backticks
- if (lang === '' && text.match(/^```/gm)) {
- return text
- .split('\n')
- .map(l => {
- const line = l.trim();
- if (line.length === 0) return '';
-
- return ` ${line}`;
- })
- .join('\n');
- }
-
- return `\`\`\`${lang}\n${text}\n\`\`\``;
- },
- 'pre > code'(el, text) {
- // Don't wrap code blocks in ``
- return text;
- },
- },
- MarkdownFilter: {
- br(el) {
- // Two spaces at the end of a line are turned into a BR
- return ' ';
- },
- code(el, text) {
- let backtickCount = 1;
- const backtickMatch = text.match(/`+/);
- if (backtickMatch) {
- backtickCount = backtickMatch[0].length + 1;
- }
-
- const backticks = Array(backtickCount + 1).join('`');
- const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
-
- return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
- },
- blockquote(el, text) {
- return text
- .trim()
- .split('\n')
- .map(s => `> ${s}`.trim())
- .join('\n');
- },
- img(el) {
- const imageSrc = el.src;
- const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
- return `![${el.getAttribute('alt')}](${imageUrl})`;
- },
- 'a.anchor'(el, text) {
- // Don't render a Markdown link for the anchor link inside a heading
- return text;
- },
- a(el, text) {
- return `[${text}](${el.getAttribute('href')})`;
- },
- li(el, text) {
- const lines = text.trim().split('\n');
- const firstLine = `- ${lines.shift()}`;
- // Add four spaces to the front of subsequent list items lines,
- // or leave the line entirely blank.
- const nextLines = lines.map(s => {
- if (s.trim().length === 0) return '';
-
- return ` ${s}`;
- });
-
- return `${firstLine}\n${nextLines.join('\n')}`;
- },
- ul(el, text) {
- return text;
- },
- ol(el, text) {
- // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
- return text.replace(/^- /gm, '1. ');
- },
- h1(el, text) {
- return `# ${text.trim()}\n`;
- },
- h2(el, text) {
- return `## ${text.trim()}\n`;
- },
- h3(el, text) {
- return `### ${text.trim()}\n`;
- },
- h4(el, text) {
- return `#### ${text.trim()}\n`;
- },
- h5(el, text) {
- return `##### ${text.trim()}\n`;
- },
- h6(el, text) {
- return `###### ${text.trim()}\n`;
- },
- strong(el, text) {
- return `**${text}**`;
- },
- em(el, text) {
- return `_${text}_`;
- },
- del(el, text) {
- return `~~${text}~~`;
- },
- hr(el) {
- // extra leading \n is to ensure that there is a blank line between
- // a list followed by an hr, otherwise this breaks old redcarpet rendering
- return '\n-----\n';
- },
- p(el, text) {
- return `${text.trim()}\n`;
- },
- table(el) {
- const theadEl = el.querySelector('thead');
- const tbodyEl = el.querySelector('tbody');
- if (!theadEl || !tbodyEl) return false;
-
- const theadText = CopyAsGFM.nodeToGFM(theadEl);
- const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
-
- return [theadText, tbodyText].join('\n');
- },
- thead(el, text) {
- const cells = _.map(el.querySelectorAll('th'), cell => {
- let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
-
- let before = '';
- let after = '';
- const alignment = cell.align || cell.style.textAlign;
-
- switch (alignment) {
- case 'center':
- before = ':';
- after = ':';
- chars -= 2;
- break;
- case 'right':
- after = ':';
- chars -= 1;
- break;
- default:
- break;
- }
-
- chars = Math.max(chars, 3);
-
- const middle = Array(chars + 1).join('-');
-
- return before + middle + after;
- });
-
- const separatorRow = `|${cells.join('|')}|`;
-
- return [text, separatorRow].join('\n');
- },
- tr(el) {
- const cellEls = el.querySelectorAll('td, th');
- if (cellEls.length === 0) return false;
-
- const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
- return `| ${cells.join(' | ')} |`;
- },
- },
-};
+import { DOMParser } from 'prosemirror-model';
+import { getSelectedFragment } from '~/lib/utils/common_utils';
+import schema from './schema';
+import markdownSerializer from './serializer';
export class CopyAsGFM {
constructor() {
@@ -347,8 +35,13 @@ export class CopyAsGFM {
e.preventDefault();
e.stopPropagation();
+ const div = document.createElement('div');
+ div.appendChild(el.cloneNode(true));
+ const html = div.innerHTML;
+
clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
+ clipboardData.setData('text/html', html);
}
static pasteGFM(e) {
@@ -361,7 +54,7 @@ export class CopyAsGFM {
e.preventDefault();
- window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
+ window.gl.utils.insertText(e.target, textBefore => {
// If the text before the cursor contains an odd number of backticks,
// we are either inside an inline code span that starts with 1 backtick
// or a code block that starts with 3 backticks.
@@ -443,75 +136,12 @@ export class CopyAsGFM {
return codeElement;
}
- static nodeToGFM(node, respectWhitespaceParam = false) {
- if (node.nodeType === Node.COMMENT_NODE) {
- return '';
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- return node.textContent;
- }
-
- const respectWhitespace =
- respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
-
- const text = this.innerGFM(node, respectWhitespace);
-
- if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
- return text;
- }
-
- for (const filter in gfmRules) {
- const rules = gfmRules[filter];
-
- for (const selector in rules) {
- const func = rules[selector];
-
- if (!nodeMatchesSelector(node, selector)) continue;
-
- let result;
- if (func.length === 2) {
- // if `func` takes 2 arguments, it depends on text.
- // if there is no text, we don't need to generate GFM for this node.
- if (text.length === 0) continue;
-
- result = func(node, text);
- } else {
- result = func(node);
- }
-
- if (result === false) continue;
-
- return result;
- }
- }
-
- return text;
- }
-
- static innerGFM(parentNode, respectWhitespace = false) {
- const nodes = parentNode.childNodes;
-
- const clonedParentNode = parentNode.cloneNode(true);
- const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
-
- for (let i = 0; i < nodes.length; i += 1) {
- const node = nodes[i];
- const clonedNode = clonedNodes[i];
-
- const text = this.nodeToGFM(node, respectWhitespace);
-
- // `clonedNode.replaceWith(text)` is not yet widely supported
- clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
- }
-
- let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
-
- if (!respectWhitespace) {
- nodeText = nodeText.trim();
- }
+ static nodeToGFM(node) {
+ const wrapEl = document.createElement('div');
+ wrapEl.appendChild(node.cloneNode(true));
+ const doc = DOMParser.fromSchema(schema).parse(wrapEl);
- return nodeText;
+ return markdownSerializer.serialize(doc);
}
}
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
new file mode 100644
index 00000000000..47e5fc65c48
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
@@ -0,0 +1,106 @@
+import Doc from './nodes/doc';
+import Paragraph from './nodes/paragraph';
+import Text from './nodes/text';
+
+import Blockquote from './nodes/blockquote';
+import CodeBlock from './nodes/code_block';
+import HardBreak from './nodes/hard_break';
+import Heading from './nodes/heading';
+import HorizontalRule from './nodes/horizontal_rule';
+import Image from './nodes/image';
+
+import Table from './nodes/table';
+import TableHead from './nodes/table_head';
+import TableBody from './nodes/table_body';
+import TableHeaderRow from './nodes/table_header_row';
+import TableRow from './nodes/table_row';
+import TableCell from './nodes/table_cell';
+
+import Emoji from './nodes/emoji';
+import Reference from './nodes/reference';
+
+import TableOfContents from './nodes/table_of_contents';
+import Video from './nodes/video';
+
+import BulletList from './nodes/bullet_list';
+import OrderedList from './nodes/ordered_list';
+import ListItem from './nodes/list_item';
+
+import DescriptionList from './nodes/description_list';
+import DescriptionTerm from './nodes/description_term';
+import DescriptionDetails from './nodes/description_details';
+
+import TaskList from './nodes/task_list';
+import OrderedTaskList from './nodes/ordered_task_list';
+import TaskListItem from './nodes/task_list_item';
+
+import Summary from './nodes/summary';
+import Details from './nodes/details';
+
+import Bold from './marks/bold';
+import Italic from './marks/italic';
+import Strike from './marks/strike';
+import InlineDiff from './marks/inline_diff';
+
+import Link from './marks/link';
+import Code from './marks/code';
+import MathMark from './marks/math';
+import InlineHTML from './marks/inline_html';
+
+// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
+// GitLab Flavored Markdown (GFM) to HTML.
+// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard.
+// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
+// from GFM should have a node or mark here.
+// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
+
+export default [
+ new Doc(),
+ new Paragraph(),
+ new Text(),
+
+ new Blockquote(),
+ new CodeBlock(),
+ new HardBreak(),
+ new Heading({ maxLevel: 6 }),
+ new HorizontalRule(),
+ new Image(),
+
+ new Table(),
+ new TableHead(),
+ new TableBody(),
+ new TableHeaderRow(),
+ new TableRow(),
+ new TableCell(),
+
+ new Emoji(),
+ new Reference(),
+
+ new TableOfContents(),
+ new Video(),
+
+ new BulletList(),
+ new OrderedList(),
+ new ListItem(),
+
+ new DescriptionList(),
+ new DescriptionTerm(),
+ new DescriptionDetails(),
+
+ new TaskList(),
+ new OrderedTaskList(),
+ new TaskListItem(),
+
+ new Summary(),
+ new Details(),
+
+ new Bold(),
+ new Italic(),
+ new Strike(),
+ new InlineDiff(),
+
+ new Link(),
+ new Code(),
+ new MathMark(),
+ new InlineHTML(),
+];
diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js
new file mode 100644
index 00000000000..b537954c1cb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/bold.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Bold as BaseBold } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Bold extends BaseBold {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.strong;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js
new file mode 100644
index 00000000000..a760ee80dd0
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/code.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Code as BaseCode } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Code extends BaseCode {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.code;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
new file mode 100644
index 00000000000..ce425e80cd3
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter
+export default class InlineDiff extends Mark {
+ get name() {
+ return 'inline_diff';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ addition: {
+ default: true,
+ },
+ },
+ parseDOM: [
+ { tag: 'span.idiff.addition', attrs: { addition: true } },
+ { tag: 'span.idiff.deletion', attrs: { addition: false } },
+ ],
+ toDOM: node => [
+ 'span',
+ { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` },
+ 0,
+ ],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark) {
+ return mark.attrs.addition ? '{+' : '{-';
+ },
+ close(state, mark) {
+ return mark.attrs.addition ? '+}' : '-}';
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
new file mode 100644
index 00000000000..ebed8698e21
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
@@ -0,0 +1,46 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+import _ from 'underscore';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class InlineHTML extends Mark {
+ get name() {
+ return 'inline_html';
+ }
+
+ get schema() {
+ return {
+ excludes: '',
+ attrs: {
+ tag: {},
+ title: { default: null },
+ },
+ parseDOM: [
+ {
+ tag: 'sup, sub, kbd, q, samp, var',
+ getAttrs: el => ({ tag: el.nodeName.toLowerCase() }),
+ },
+ {
+ tag: 'abbr',
+ getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }),
+ },
+ ],
+ toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark) {
+ return `<${mark.attrs.tag}${
+ mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : ''
+ }>`;
+ },
+ close(state, mark) {
+ return `</${mark.attrs.tag}>`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js
new file mode 100644
index 00000000000..44b35c97739
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/italic.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { Italic as BaseItalic } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Italic extends BaseItalic {
+ get toMarkdown() {
+ return defaultMarkdownSerializer.marks.em;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js
new file mode 100644
index 00000000000..5c23d6a5ceb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/link.js
@@ -0,0 +1,21 @@
+/* eslint-disable class-methods-use-this */
+
+import { Link as BaseLink } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Link extends BaseLink {
+ get toMarkdown() {
+ return {
+ mixable: true,
+ open(state, mark, parent, index) {
+ const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index);
+ return open === '<' ? '' : open;
+ },
+ close(state, mark, parent, index) {
+ const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index);
+ return close === '>' ? '' : close;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js
new file mode 100644
index 00000000000..e582fb18f15
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/math.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Mark } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
+export default class MathMark extends Mark {
+ get name() {
+ return 'math';
+ }
+
+ get schema() {
+ return {
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::MathFilter
+ {
+ tag: 'code.code.math[data-math-style=inline]',
+ priority: 51,
+ },
+ // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex',
+ contentElement: 'annotation[encoding="application/x-tex"]',
+ },
+ ],
+ toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0],
+ };
+ }
+
+ get toMarkdown() {
+ return {
+ escape: false,
+ open(state, mark, parent, index) {
+ return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`;
+ },
+ close(state, mark, parent, index) {
+ return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`;
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js
new file mode 100644
index 00000000000..c2951a40a4b
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js
@@ -0,0 +1,15 @@
+/* eslint-disable class-methods-use-this */
+
+import { Strike as BaseStrike } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Strike extends BaseStrike {
+ get toMarkdown() {
+ return {
+ open: '~~',
+ close: '~~',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
new file mode 100644
index 00000000000..b0bc8f79643
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js
@@ -0,0 +1,13 @@
+/* eslint-disable class-methods-use-this */
+
+import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Blockquote extends BaseBlockquote {
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ defaultMarkdownSerializer.nodes.blockquote(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
new file mode 100644
index 00000000000..3b0792e1af8
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { BulletList as BaseBulletList } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class BulletList extends BaseBulletList {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.bullet_list(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
new file mode 100644
index 00000000000..1e0c05eff08
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js
@@ -0,0 +1,99 @@
+/* eslint-disable class-methods-use-this */
+
+import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions';
+
+const PLAINTEXT_LANG = 'plaintext';
+
+// Transforms generated HTML back to GFM for:
+// - Banzai::Filter::SyntaxHighlightFilter
+// - Banzai::Filter::MathFilter
+// - Banzai::Filter::MermaidFilter
+// - Banzai::Filter::SuggestionFilter
+export default class CodeBlock extends BaseCodeBlock {
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ group: 'block',
+ code: true,
+ defining: true,
+ attrs: {
+ lang: { default: PLAINTEXT_LANG },
+ },
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter
+ {
+ tag: 'pre.code.highlight',
+ preserveWhitespace: 'full',
+ getAttrs: el => {
+ const lang = el.getAttribute('lang');
+ if (!lang || lang === '') return {};
+
+ return { lang };
+ },
+ },
+ // Matches HTML generated by Banzai::Filter::MathFilter,
+ // after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
+ {
+ tag: 'span.katex-display',
+ preserveWhitespace: 'full',
+ contentElement: 'annotation[encoding="application/x-tex"]',
+ attrs: { lang: 'math' },
+ },
+ // Matches HTML generated by Banzai::Filter::MermaidFilter,
+ // after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js
+ {
+ tag: 'svg.mermaid',
+ preserveWhitespace: 'full',
+ contentElement: 'text.source',
+ attrs: { lang: 'mermaid' },
+ },
+ // Matches HTML generated by Banzai::Filter::SuggestionFilter,
+ // after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+ {
+ tag: '.md-suggestion',
+ skip: true,
+ },
+ {
+ tag: '.md-suggestion-header',
+ ignore: true,
+ },
+ {
+ tag: '.md-suggestion-diff',
+ preserveWhitespace: 'full',
+ getContent: (el, schema) =>
+ [...el.querySelectorAll('.line_content.new span')].map(span =>
+ schema.text(span.innerText),
+ ),
+ attrs: { lang: 'suggestion' },
+ },
+ ],
+ toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]],
+ };
+ }
+
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ const {
+ textContent: text,
+ attrs: { lang },
+ } = node;
+
+ // Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks
+ if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) {
+ state.wrapBlock(' ', null, node, () => state.text(text, false));
+ return;
+ }
+
+ state.write('```');
+ if (lang !== PLAINTEXT_LANG) state.write(lang);
+
+ state.ensureNewLine();
+ state.text(text, false);
+ state.ensureNewLine();
+
+ state.write('```');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
new file mode 100644
index 00000000000..a4451d8ce8d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionDetails extends Node {
+ get name() {
+ return 'description_details';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'dd' }],
+ toDOM: () => ['dd', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.write('<dd>');
+ state.text(node.textContent, false);
+ state.write('</dd>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
new file mode 100644
index 00000000000..6aa1aca29d7
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionList extends Node {
+ get name() {
+ return 'description_list';
+ }
+
+ get schema() {
+ return {
+ content: '(description_term+ description_details+)+',
+ group: 'block',
+ parseDOM: [{ tag: 'dl' }],
+ toDOM: () => ['dl', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<dl>\n');
+ state.wrapBlock(' ', null, node, () => state.renderContent(node));
+ state.flushClose(1);
+ state.ensureNewLine();
+ state.write('</dl>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
new file mode 100644
index 00000000000..89057ec6444
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class DescriptionTerm extends Node {
+ get name() {
+ return 'description_term';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'dt' }],
+ toDOM: () => ['dt', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2);
+ state.write('<dt>');
+ state.text(node.textContent, false);
+ state.write('</dt>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/details.js b/app/assets/javascripts/behaviors/markdown/nodes/details.js
new file mode 100644
index 00000000000..1c40dbb8168
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/details.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Details extends Node {
+ get name() {
+ return 'details';
+ }
+
+ get schema() {
+ return {
+ content: 'summary block*',
+ group: 'block',
+ parseDOM: [{ tag: 'details' }],
+ toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<details>\n');
+ state.renderContent(node);
+ state.flushClose(1);
+ state.ensureNewLine();
+ state.write('</details>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/doc.js b/app/assets/javascripts/behaviors/markdown/nodes/doc.js
new file mode 100644
index 00000000000..88b16fd85da
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/doc.js
@@ -0,0 +1,15 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+export default class Doc extends Node {
+ get name() {
+ return 'doc';
+ }
+
+ get schema() {
+ return {
+ content: 'block+',
+ };
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
new file mode 100644
index 00000000000..a7cc3e828f5
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js
@@ -0,0 +1,41 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter
+export default class Emoji extends Node {
+ get name() {
+ return 'emoji';
+ }
+
+ get schema() {
+ return {
+ inline: true,
+ group: 'inline',
+ attrs: {
+ name: {},
+ title: {},
+ moji: {},
+ },
+ parseDOM: [
+ {
+ tag: 'gl-emoji',
+ getAttrs: el => ({
+ name: el.dataset.name,
+ title: el.getAttribute('title'),
+ moji: el.textContent,
+ }),
+ },
+ ],
+ toDOM: node => [
+ 'gl-emoji',
+ { 'data-name': node.attrs.name, title: node.attrs.title },
+ node.attrs.moji,
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(`:${node.attrs.name}:`);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
new file mode 100644
index 00000000000..59e5d8ab3e2
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js
@@ -0,0 +1,10 @@
+/* eslint-disable class-methods-use-this */
+
+import { HardBreak as BaseHardBreak } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class HardBreak extends BaseHardBreak {
+ toMarkdown(state) {
+ if (!state.atBlank()) state.write(' \n');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
new file mode 100644
index 00000000000..fec8608cf5d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/heading.js
@@ -0,0 +1,13 @@
+/* eslint-disable class-methods-use-this */
+
+import { Heading as BaseHeading } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Heading extends BaseHeading {
+ toMarkdown(state, node) {
+ if (!node.childCount) return;
+
+ defaultMarkdownSerializer.nodes.heading(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
new file mode 100644
index 00000000000..695c7160bde
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class HorizontalRule extends BaseHorizontalRule {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.horizontal_rule(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js
new file mode 100644
index 00000000000..c225a5ed876
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js
@@ -0,0 +1,52 @@
+/* eslint-disable class-methods-use-this */
+
+import { Image as BaseImage } from 'tiptap-extensions';
+import { placeholderImage } from '~/lazy_loader';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+export default class Image extends BaseImage {
+ get schema() {
+ return {
+ attrs: {
+ src: {},
+ alt: {
+ default: null,
+ },
+ title: {
+ default: null,
+ },
+ },
+ group: 'inline',
+ inline: true,
+ draggable: true,
+ parseDOM: [
+ // Matches HTML generated by Banzai::Filter::ImageLinkFilter
+ {
+ tag: 'a.no-attachment-icon',
+ priority: 51,
+ skip: true,
+ },
+ // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
+ {
+ tag: 'img[src]',
+ getAttrs: el => {
+ const imageSrc = el.src;
+ const imageUrl =
+ imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
+
+ return {
+ src: imageUrl,
+ title: el.getAttribute('title'),
+ alt: el.getAttribute('alt'),
+ };
+ },
+ },
+ ],
+ toDOM: node => ['img', node.attrs],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.image(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
new file mode 100644
index 00000000000..4237637ed9a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js
@@ -0,0 +1,11 @@
+/* eslint-disable class-methods-use-this */
+
+import { ListItem as BaseListItem } from 'tiptap-extensions';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class ListItem extends BaseListItem {
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.list_item(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
new file mode 100644
index 00000000000..4c1542d14ea
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js
@@ -0,0 +1,10 @@
+/* eslint-disable class-methods-use-this */
+
+import { OrderedList as BaseOrderedList } from 'tiptap-extensions';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class OrderedList extends BaseOrderedList {
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '1. ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
new file mode 100644
index 00000000000..25c4976a1bc
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class OrderedTaskList extends Node {
+ get name() {
+ return 'ordered_task_list';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ content: '(task_list_item|list_item)+',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'ol.task-list',
+ },
+ ],
+ toDOM: () => ['ol', { class: 'task-list' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '1. ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
new file mode 100644
index 00000000000..dec3207b1bb
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Paragraph extends Node {
+ get name() {
+ return 'paragraph';
+ }
+
+ get schema() {
+ return {
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p' }],
+ toDOM: () => ['p', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.paragraph(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js
new file mode 100644
index 00000000000..5d6bbeca833
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/reference.js
@@ -0,0 +1,52 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
+export default class Reference extends Node {
+ get name() {
+ return 'reference';
+ }
+
+ get schema() {
+ return {
+ inline: true,
+ group: 'inline',
+ atom: true,
+ attrs: {
+ className: {},
+ referenceType: {},
+ originalText: { default: null },
+ href: {},
+ text: {},
+ },
+ parseDOM: [
+ {
+ tag: 'a.gfm:not([data-link=true])',
+ priority: 51,
+ getAttrs: el => ({
+ className: el.className,
+ referenceType: el.dataset.referenceType,
+ originalText: el.dataset.original,
+ href: el.getAttribute('href'),
+ text: el.textContent,
+ }),
+ },
+ ],
+ toDOM: node => [
+ 'a',
+ {
+ class: node.attrs.className,
+ href: node.attrs.href,
+ 'data-reference-type': node.attrs.referenceType,
+ 'data-original': node.attrs.originalText,
+ },
+ node.attrs.text,
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(node.attrs.originalText || node.attrs.text);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/summary.js b/app/assets/javascripts/behaviors/markdown/nodes/summary.js
new file mode 100644
index 00000000000..2e36e316d71
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/summary.js
@@ -0,0 +1,27 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Summary extends Node {
+ get name() {
+ return 'summary';
+ }
+
+ get schema() {
+ return {
+ content: 'text*',
+ marks: '',
+ defining: true,
+ parseDOM: [{ tag: 'summary' }],
+ toDOM: () => ['summary', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('<summary>');
+ state.text(node.textContent, false);
+ state.write('</summary>');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table.js b/app/assets/javascripts/behaviors/markdown/nodes/table.js
new file mode 100644
index 00000000000..a7fcb9227cd
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table.js
@@ -0,0 +1,25 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class Table extends Node {
+ get name() {
+ return 'table';
+ }
+
+ get schema() {
+ return {
+ content: 'table_head table_body',
+ group: 'block',
+ isolating: true,
+ parseDOM: [{ tag: 'table' }],
+ toDOM: () => ['table', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
new file mode 100644
index 00000000000..403556dc0c8
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableBody extends Node {
+ get name() {
+ return 'table_body';
+ }
+
+ get schema() {
+ return {
+ content: 'table_row+',
+ parseDOM: [{ tag: 'tbody' }],
+ toDOM: () => ['tbody', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
new file mode 100644
index 00000000000..c63bfe10e39
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js
@@ -0,0 +1,35 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableCell extends Node {
+ get name() {
+ return 'table_cell';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ header: { default: false },
+ align: { default: null },
+ },
+ content: 'inline*',
+ isolating: true,
+ parseDOM: [
+ {
+ tag: 'td, th',
+ getAttrs: el => ({
+ header: el.tagName === 'TH',
+ align: el.getAttribute('align') || el.style.textAlign,
+ }),
+ },
+ ],
+ toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderInline(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
new file mode 100644
index 00000000000..4cb94bf088c
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js
@@ -0,0 +1,24 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableHead extends Node {
+ get name() {
+ return 'table_head';
+ }
+
+ get schema() {
+ return {
+ content: 'table_header_row',
+ parseDOM: [{ tag: 'thead' }],
+ toDOM: () => ['thead', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.flushClose(1);
+ state.renderContent(node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
new file mode 100644
index 00000000000..e7eee636402
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js
@@ -0,0 +1,43 @@
+/* eslint-disable class-methods-use-this */
+
+import TableRow from './table_row';
+
+const CENTER_ALIGN = 'center';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableHeaderRow extends TableRow {
+ get name() {
+ return 'table_header_row';
+ }
+
+ get schema() {
+ return {
+ content: 'table_cell+',
+ parseDOM: [
+ {
+ tag: 'thead tr',
+ priority: 51,
+ },
+ ],
+ toDOM: () => ['tr', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ const cellWidths = super.toMarkdown(state, node);
+
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
new file mode 100644
index 00000000000..20c7fa8a9ab
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
@@ -0,0 +1,33 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
+export default class TableOfContents extends Node {
+ get name() {
+ return 'table_of_contents';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ atom: true,
+ parseDOM: [
+ {
+ tag: 'ul.section-nav',
+ priority: 51,
+ },
+ {
+ tag: 'p.table-of-contents',
+ priority: 51,
+ },
+ ],
+ toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write('[[_TOC_]]');
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
new file mode 100644
index 00000000000..5852502773a
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js
@@ -0,0 +1,38 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
+export default class TableRow extends Node {
+ get name() {
+ return 'table_row';
+ }
+
+ get schema() {
+ return {
+ content: 'table_cell+',
+ parseDOM: [{ tag: 'tr' }],
+ toDOM: () => ['tr', 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ const cellWidths = [];
+
+ state.flushClose(1);
+
+ state.write('| ');
+ node.forEach((cell, _, i) => {
+ if (i) state.write(' | ');
+
+ const { length } = state.out;
+ state.render(cell, node, i);
+ cellWidths.push(state.out.length - length);
+ });
+ state.write(' |');
+
+ state.closeBlock(node);
+
+ return cellWidths;
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
new file mode 100644
index 00000000000..ab33bc21502
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js
@@ -0,0 +1,28 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class TaskList extends Node {
+ get name() {
+ return 'task_list';
+ }
+
+ get schema() {
+ return {
+ group: 'block',
+ content: '(task_list_item|list_item)+',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'ul.task-list',
+ },
+ ],
+ toDOM: () => ['ul', { class: 'task-list' }, 0],
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.renderList(node, ' ', () => '* ');
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
new file mode 100644
index 00000000000..d0ee7333d5e
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
@@ -0,0 +1,49 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
+export default class TaskListItem extends Node {
+ get name() {
+ return 'task_list_item';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ done: {
+ default: false,
+ },
+ },
+ defining: true,
+ draggable: false,
+ content: 'paragraph block*',
+ parseDOM: [
+ {
+ priority: 51,
+ tag: 'li.task-list-item',
+ getAttrs: el => {
+ const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
+ return { done: checkbox && checkbox.checked };
+ },
+ },
+ ],
+ toDOM(node) {
+ return [
+ 'li',
+ { class: 'task-list-item' },
+ [
+ 'input',
+ { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done },
+ ],
+ ['div', { class: 'todo-content' }, 0],
+ ];
+ },
+ };
+ }
+
+ toMarkdown(state, node) {
+ state.write(`[${node.attrs.done ? 'x' : ' '}] `);
+ state.renderContent(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js
new file mode 100644
index 00000000000..84838c14999
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/text.js
@@ -0,0 +1,20 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+export default class Text extends Node {
+ get name() {
+ return 'text';
+ }
+
+ get schema() {
+ return {
+ group: 'inline',
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.text(state, node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/video.js b/app/assets/javascripts/behaviors/markdown/nodes/video.js
new file mode 100644
index 00000000000..516f983397d
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/nodes/video.js
@@ -0,0 +1,54 @@
+/* eslint-disable class-methods-use-this */
+
+import { Node } from 'tiptap';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown';
+
+// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter
+export default class Video extends Node {
+ get name() {
+ return 'video';
+ }
+
+ get schema() {
+ return {
+ attrs: {
+ src: {},
+ alt: {
+ default: null,
+ },
+ },
+ group: 'block',
+ draggable: true,
+ parseDOM: [
+ {
+ tag: '.video-container',
+ skip: true,
+ },
+ {
+ tag: '.video-container p',
+ priority: 51,
+ ignore: true,
+ },
+ {
+ tag: 'video[src]',
+ getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
+ },
+ ],
+ toDOM: node => [
+ 'video',
+ {
+ src: node.attrs.src,
+ width: '400',
+ controls: true,
+ 'data-setup': '{}',
+ 'data-title': node.attrs.alt,
+ },
+ ],
+ };
+ }
+
+ toMarkdown(state, node) {
+ defaultMarkdownSerializer.nodes.image(state, node);
+ state.closeBlock(node);
+ }
+}
diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js
new file mode 100644
index 00000000000..163182ab778
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/schema.js
@@ -0,0 +1,24 @@
+import { Schema } from 'prosemirror-model';
+import editorExtensions from './editor_extensions';
+
+const nodes = editorExtensions
+ .filter(extension => extension.type === 'node')
+ .reduce(
+ (ns, { name, schema }) => ({
+ ...ns,
+ [name]: schema,
+ }),
+ {},
+ );
+
+const marks = editorExtensions
+ .filter(extension => extension.type === 'mark')
+ .reduce(
+ (ms, { name, schema }) => ({
+ ...ms,
+ [name]: schema,
+ }),
+ {},
+ );
+
+export default new Schema({ nodes, marks });
diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js
new file mode 100644
index 00000000000..70dbd8bd206
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/serializer.js
@@ -0,0 +1,24 @@
+import { MarkdownSerializer } from 'prosemirror-markdown';
+import editorExtensions from './editor_extensions';
+
+const nodes = editorExtensions
+ .filter(extension => extension.type === 'node')
+ .reduce(
+ (ns, { name, toMarkdown }) => ({
+ ...ns,
+ [name]: toMarkdown,
+ }),
+ {},
+ );
+
+const marks = editorExtensions
+ .filter(extension => extension.type === 'mark')
+ .reduce(
+ (ms, { name, toMarkdown }) => ({
+ ...ms,
+ [name]: toMarkdown,
+ }),
+ {},
+ );
+
+export default new MarkdownSerializer(nodes, marks);
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 2918e1486a7..0eb067d4963 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
-import _ from 'underscore';
import Sidebar from '../../right_sidebar';
import Shortcuts from './shortcuts';
import { CopyAsGFM } from '../markdown/copy_as_gfm';
@@ -63,18 +62,18 @@ export default class ShortcutsIssuable extends Shortcuts {
}
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- const selected = CopyAsGFM.nodeToGFM(el);
+ const blockquoteEl = document.createElement('blockquote');
+ blockquoteEl.appendChild(el);
+ const text = CopyAsGFM.nodeToGFM(blockquoteEl);
- if (selected.trim() === '') {
+ if (text.trim() === '') {
return false;
}
- const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
-
// If replyField already has some content, add a newline before our quote
const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
$replyField
- .val((a, current) => `${current}${separator}${quote.join('')}\n`)
+ .val((a, current) => `${current}${separator}${text}\n\n`)
.trigger('input')
.trigger('change');
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index a7ed175f7a4..009153d0703 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -7,4 +7,3 @@ import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
-import 'select2/select2';
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 3770b5c8864..3ef54752436 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -2,10 +2,10 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
+import SettingsDropdown from './settings_dropdown.vue';
export default {
components: {
@@ -13,6 +13,7 @@ export default {
Icon,
GlLink,
GlButton,
+ SettingsDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,23 +36,10 @@ export default {
},
computed: {
...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']),
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']),
+ ...mapGetters('diffs', ['hasCollapsedFile']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
- toggleWhitespaceText() {
- if (this.isWhitespaceVisible()) {
- return __('Hide whitespace changes');
- }
- return __('Show whitespace changes');
- },
- toggleWhitespacePath() {
- if (this.isWhitespaceVisible()) {
- return mergeUrlParams({ w: 1 }, window.location.href);
- }
-
- return mergeUrlParams({ w: 0 }, window.location.href);
- },
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
@@ -75,9 +63,6 @@ export default {
'expandAllFiles',
'toggleShowTreeList',
]),
- isWhitespaceVisible() {
- return getParameterValues('w')[0] !== '1';
- },
},
};
</script>
@@ -118,7 +103,7 @@ export default {
{{ __('Viewing commit') }}
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
</div>
- <div class="inline-parallel-buttons d-none d-lg-flex ml-auto">
+ <div class="inline-parallel-buttons d-none d-md-flex ml-auto">
<gl-button
v-if="commit || startVersion"
:href="latestVersionPath"
@@ -129,31 +114,7 @@ export default {
<a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles">
{{ __('Expand all') }}
</a>
- <a :href="toggleWhitespacePath" class="btn btn-default qa-toggle-whitespace">
- {{ toggleWhitespaceText }}
- </a>
- <div class="btn-group prepend-left-8">
- <button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- type="button"
- class="btn js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </button>
- <button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- type="button"
- class="btn js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </button>
- </div>
+ <settings-dropdown />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
new file mode 100644
index 00000000000..0129763161a
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -0,0 +1,92 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ GlButton,
+ Icon,
+ },
+ computed: {
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
+ ...mapState('diffs', ['renderTreeList', 'showWhitespace']),
+ },
+ methods: {
+ ...mapActions('diffs', [
+ 'setInlineDiffViewType',
+ 'setParallelDiffViewType',
+ 'setRenderTreeList',
+ 'setShowWhitespace',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-default js-show-diff-settings"
+ data-toggle="dropdown"
+ data-display="static"
+ >
+ <icon name="settings" /> <icon name="arrow-down" />
+ </button>
+ <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
+ <div>
+ <span class="bold d-block mb-1">{{ __('File browser') }}</span>
+ <div class="btn-group d-flex">
+ <gl-button
+ :class="{ active: !renderTreeList }"
+ class="w-100 js-list-view"
+ @click="setRenderTreeList(false)"
+ >
+ {{ __('List view') }}
+ </gl-button>
+ <gl-button
+ :class="{ active: renderTreeList }"
+ class="w-100 js-tree-view"
+ @click="setRenderTreeList(true)"
+ >
+ {{ __('Tree view') }}
+ </gl-button>
+ </div>
+ </div>
+ <div class="mt-2">
+ <span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
+ <div class="btn-group d-flex js-diff-view-buttons">
+ <gl-button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ class="w-100 js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </gl-button>
+ <gl-button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ class="w-100 js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </gl-button>
+ </div>
+ </div>
+ <div class="mt-2">
+ <label class="mb-0">
+ <input
+ id="show-whitespace"
+ type="checkbox"
+ :checked="showWhitespace"
+ @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })"
+ />
+ {{ __('Show whitespace changes') }}
+ </label>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 097587c5ac1..0b3def3d29d 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,13 +1,10 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
-const treeListStorageKey = 'mr_diff_tree_list';
-
export default {
directives: {
GlTooltip: GlTooltipDirective,
@@ -17,17 +14,12 @@ export default {
FileRow,
},
data() {
- const treeListStored = localStorage.getItem(treeListStorageKey);
- const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
-
return {
search: '',
- renderTreeList,
- focusSearch: false,
};
},
computed: {
- ...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
+ ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']),
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
@@ -52,19 +44,6 @@ export default {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
- this.toggleFocusSearch(false);
- },
- toggleRenderTreeList(toggle) {
- this.renderTreeList = toggle;
- localStorage.setItem(treeListStorageKey, this.renderTreeList);
- },
- toggleFocusSearch(toggle) {
- this.focusSearch = toggle;
- },
- blurSearch() {
- if (this.search.trim() === '') {
- this.toggleFocusSearch(false);
- }
},
},
FileRowStats,
@@ -81,8 +60,6 @@ export default {
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
- @focus="toggleFocusSearch(true)"
- @blur="blurSearch"
/>
<button
v-show="search"
@@ -94,34 +71,6 @@ export default {
<icon name="close" />
</button>
</div>
- <div v-show="!focusSearch" class="btn-group prepend-left-8 tree-list-view-toggle">
- <button
- v-gl-tooltip.hover
- :aria-label="__('List view')"
- :title="__('List view')"
- :class="{
- active: !renderTreeList,
- }"
- class="btn btn-default pt-0 pb-0 d-flex align-items-center"
- type="button"
- @click="toggleRenderTreeList(false)"
- >
- <icon name="hamburger" />
- </button>
- <button
- v-gl-tooltip.hover
- :aria-label="__('Tree view')"
- :title="__('Tree view')"
- :class="{
- active: renderTreeList,
- }"
- class="btn btn-default pt-0 pb-0 d-flex align-items-center"
- type="button"
- @click="toggleRenderTreeList(true)"
- >
- <icon name="file-tree" />
- </button>
- </div>
</div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
<template v-if="filteredTreeList.length">
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 0af1ba13d36..bd188d9de9e 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -34,3 +34,5 @@ export const MAX_LINES_TO_BE_RENDERED = 2000;
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export const TREE_TYPE = 'tree';
+export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
+export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index b130cedc24c..094e5cdea9c 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
-import { mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
import diffsApp from './components/app.vue';
+import { TREE_LIST_STORAGE_KEY } from './constants';
export default function initDiffsApp(store) {
return new Vue({
@@ -26,6 +29,16 @@ export default function initDiffsApp(store) {
activeTab: state => state.page.activeTab,
}),
},
+ created() {
+ const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY);
+ const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
+
+ this.setRenderTreeList(renderTreeList);
+ this.setShowWhitespace({ showWhitespace: getParameterValues('w')[0] !== '1' });
+ },
+ methods: {
+ ...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']),
+ },
render(createElement) {
return createElement('diffs-app', {
props: {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 196c9dfb1c2..2c5019fb652 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -14,6 +14,8 @@ import {
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
+ TREE_LIST_STORAGE_KEY,
+ WHITESPACE_STORAGE_KEY,
} from '../constants';
export const setBaseConfig = ({ commit }, options) => {
@@ -33,7 +35,7 @@ export const fetchDiffFiles = ({ state, commit }) => {
});
return axios
- .get(state.endpoint)
+ .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } })
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
@@ -278,5 +280,21 @@ export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
};
+export const setRenderTreeList = ({ commit }, renderTreeList) => {
+ commit(types.SET_RENDER_TREE_LIST, renderTreeList);
+
+ localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList);
+};
+
+export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => {
+ commit(types.SET_SHOW_WHITESPACE, showWhitespace);
+
+ localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace);
+
+ if (pushState) {
+ historyPushState(showWhitespace ? '?w=0' : '?w=1');
+ }
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 98e57d52d77..05b4c552f6e 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -27,4 +27,6 @@ export default () => ({
projectPath: '',
commentForms: [],
highlightedRow: null,
+ renderTreeList: true,
+ showWhitespace: true,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 6ed8c5709a8..e760b4d1079 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -20,3 +20,5 @@ export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
export const SET_TREE_DATA = 'SET_TREE_DATA';
+export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
+export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 00095997ba3..4aeb393b29b 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -238,4 +238,10 @@ export default {
state.treeEntries = treeEntries;
state.tree = tree;
},
+ [types.SET_RENDER_TREE_LIST](state, renderTreeList) {
+ state.renderTreeList = renderTreeList;
+ },
+ [types.SET_SHOW_WHITESPACE](state, showWhitespace) {
+ state.showWhitespace = showWhitespace;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 09afacc24df..effb6202327 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
import { diffModes } from '~/ide/constants';
+import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -306,7 +307,7 @@ export const getLowestSingleFolder = folder => {
if (shouldGetFolder) {
const firstFolder = getFolder(file);
- path.push(firstFolder.path);
+ path.push(...firstFolder.path);
tree.push(...firstFolder.tree);
}
@@ -321,7 +322,7 @@ export const getLowestSingleFolder = folder => {
const { path, tree } = getFolder(folder, [folder.name]);
return {
- path: path.join('/'),
+ path: truncatePathMiddleToLength(path.join('/'), 40),
treeAcc: tree.length ? tree[tree.length - 1].tree : null,
};
};
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 96dc1f07cb9..e81a1525df0 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -143,7 +143,7 @@ export default {
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope };
+ this.requestData = { page: this.page, scope: this.scope, nested: true };
this.poll = new Poll({
resource: this.service,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 4e07ccba91a..cb4ff6856db 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -7,8 +7,8 @@ export default class EnvironmentsService {
}
fetchEnvironments(options = {}) {
- const { scope, page } = options;
- return axios.get(this.environmentsEndpoint, { params: { scope, page } });
+ const { scope, page, nested } = options;
+ return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 5808a2d4afa..ac9a31c202c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -20,7 +20,8 @@ export default class EnvironmentsStore {
*
* Stores the received environments.
*
- * In the main environments endpoint, each environment has the following schema
+ * In the main environments endpoint (with { nested: true } in params), each folder
+ * has the following schema:
* { name: String, size: Number, latest: Object }
* In the endpoint to retrieve environments from each folder, the environment does
* not have the `latest` key and the data is all in the root level.
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 4a2af02b40a..33c82778c79 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -593,7 +593,7 @@ export default class FilteredSearchManager {
tokens.forEach(token => {
const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
token.key,
- token.value.toLowerCase(),
+ token.value,
);
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index e01dedbb57c..b70da240833 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -65,8 +65,10 @@ export default class FilteredSearchTokenKeys {
searchByConditionKeyValue(key, value) {
return (
- this.conditions.find(condition => condition.tokenKey === key && condition.value === value) ||
- null
+ this.conditions.find(
+ condition =>
+ condition.tokenKey === key && condition.value.toLowerCase() === value.toLowerCase(),
+ ) || null
);
}
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 b494b7e2de0..fd61030eb13 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
@@ -60,52 +60,52 @@ export const conditions = [
{
url: 'assignee_id=None',
tokenKey: 'assignee',
- value: 'none',
+ value: 'None',
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
- value: 'any',
+ value: 'Any',
},
{
url: 'milestone_title=None',
tokenKey: 'milestone',
- value: 'none',
+ value: 'None',
},
{
url: 'milestone_title=Any',
tokenKey: 'milestone',
- value: 'any',
+ value: 'Any',
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
- value: 'upcoming',
+ value: 'Upcoming',
},
{
url: 'milestone_title=%23started',
tokenKey: 'milestone',
- value: 'started',
+ value: 'Started',
},
{
url: 'label_name[]=None',
tokenKey: 'label',
- value: 'none',
+ value: 'None',
},
{
url: 'label_name[]=Any',
- tokenKey: 'any',
- value: 'any',
+ tokenKey: 'label',
+ value: 'Any',
},
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
- value: 'none',
+ value: 'None',
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
- value: 'any',
+ value: 'Any',
},
];
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 2049760fe29..bdadbb1bb2a 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,93 +4,97 @@ import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
export default function groupsSelect() {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- $select.select2({
- placeholder: 'Search for a group',
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $select.select2({
+ placeholder: 'Search for a group',
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${
- object.full_name
- }</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return object.full_name;
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${
+ object.full_name
+ }</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return object.full_name;
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index f1d40586903..ce577ae85b0 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -107,16 +107,23 @@ export default {
class="commit-sha"
>{{ lastCommit.short_id }}</a
>
- by {{ lastCommit.author_name }}
+ by
+ <user-avatar-image
+ css-classes="ide-status-avatar"
+ :size="18"
+ :img-src="latestPipeline && latestPipeline.commit.author_gravatar_url"
+ :img-alt="lastCommit.author_name"
+ :tooltip-text="lastCommit.author_name"
+ />
+ {{ lastCommit.author_name }}
<time
v-tooltip
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
data-placement="top"
data-container="body"
+ >{{ lastCommitFormatedAge }}</time
>
- {{ lastCommitFormatedAge }}
- </time>
</div>
<div v-if="file" class="ide-status-file">{{ file.name }}</div>
<div v-if="file" class="ide-status-file">{{ file.eol }}</div>
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 60bddb34977..a15f04075d9 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,12 +13,12 @@ export default class Model {
(this.originalModel = monacoEditor.createModel(
head ? head.content : this.file.raw,
undefined,
- new Uri(false, false, `original/${this.path}`),
+ new Uri('gitlab', false, `original/${this.path}`),
)),
(this.model = monacoEditor.createModel(
this.content,
undefined,
- new Uri(false, false, this.path),
+ new Uri('gitlab', false, this.path),
)),
);
if (this.file.mrChange) {
@@ -26,7 +26,7 @@ export default class Model {
(this.baseModel = monacoEditor.createModel(
this.file.baseRaw,
undefined,
- new Uri(false, false, `target/${this.path}`),
+ new Uri('gitlab', false, `target/${this.path}`),
)),
);
}
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index 612c524ca1c..e0fb58ef195 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -11,10 +11,14 @@ class AutoWidthDropdownSelect {
init() {
const { dropdownClass } = this;
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
return this;
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index f3d722409b0..48e7ed1318d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -7,10 +7,14 @@ export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index c81a2230310..4d2533d01f1 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -120,35 +120,39 @@ export default class IssuableForm {
}
initTargetBranchDropdown() {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
}
}
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index e4e2eab2acd..cd569eb3045 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -10,6 +10,7 @@ import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
+import { __ } from '~/locale';
export default {
components: {
@@ -201,8 +202,8 @@ export default {
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
- if (this.showForm && this.issueChanged) {
- event.returnValue = 'Are you sure you want to lose your issue information?';
+ if (this.showForm && this.issueChanged && !this.showRecaptcha) {
+ event.returnValue = __('Are you sure you want to lose your issue information?');
}
return undefined;
},
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 062501d1d04..f134a54dd53 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -70,7 +70,18 @@ export default class LabelManager {
const $detachedLabel = $label.detach();
this.toggleLabelPriorityBadge($detachedLabel, action);
- $detachedLabel.appendTo($target);
+
+ const $labelEls = $target.find('li.label-list-item');
+
+ /*
+ * If there is a label element in the target, we'd want to
+ * append the new label just right next to it.
+ */
+ if ($labelEls.length) {
+ $labelEls.last().after($detachedLabel);
+ } else {
+ $detachedLabel.appendTo($target);
+ }
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 3b6a57dad44..ae8b4b4d635 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -614,10 +614,18 @@ export const spriteIcon = (icon, className = '') => {
/**
* This method takes in object with snake_case property names
- * and returns new object with camelCase property names
+ * and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
+ *
+ * This method also supports additional params in `options` object
+ *
+ * @param {Object} obj - Object to be converted.
+ * @param {Object} options - Object containing additional options.
+ * @param {boolean} options.deep - FLag to allow deep object converting
+ * @param {Array[]} dropKeys - List of properties to discard while building new object
+ * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
@@ -625,12 +633,26 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}
const initial = Array.isArray(obj) ? [] : {};
+ const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
- if (options.deep && (isObject(val) || Array.isArray(val))) {
+ // Drop properties from new object if
+ // there are any mentioned in options
+ if (dropKeys.indexOf(prop) > -1) {
+ return acc;
+ }
+
+ // Skip converting properties in new object
+ // if there are any mentioned in options
+ if (ignoreKeyNames.indexOf(prop) > -1) {
+ result[prop] = obj[prop];
+ return acc;
+ }
+
+ if (deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
} else {
result[convertToCamelCase(prop)] = obj[prop];
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 4ba3543f9b2..8e10b3ad912 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -100,18 +100,24 @@ function deferredInitialisation() {
});
// Initialize select2 selects
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
-
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ if ($('select.select2').length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ setTimeout(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ }, 1);
+ });
+ })
+ .catch(() => {});
+ }
// Initialize tooltips
$body.tooltip({
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b0dc5697018..2f15da42271 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -428,7 +428,7 @@ export default class MergeRequestTabs {
}
diffViewType() {
- return $('.inline-parallel-buttons button.active').data('viewType');
+ return $('.js-diff-view-buttons button.active').data('viewType');
}
isDiffAction(action) {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index cea5c1a56ca..973fc8e10c9 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -196,13 +196,13 @@ export default {
class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"
>
<ul>
- <li v-for="environment in store.environmentsData" :key="environment.latest.id">
+ <li v-for="environment in store.environmentsData" :key="environment.id">
<a
- :href="environment.latest.metrics_path"
- :class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
+ :href="environment.metrics_path"
+ :class="{ 'is-active': environment.name == currentEnvironmentName }"
class="dropdown-item"
>
- {{ environment.latest.name }}
+ {{ environment.name }}
</a>
</li>
</ul>
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 8692c873a41..96ecc5ab8a8 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -66,9 +66,7 @@ export default class MonitoringStore {
}
storeEnvironmentsData(environmentsData = []) {
- this.environmentsData = environmentsData.filter(
- environment => !!environment.latest.last_deployment,
- );
+ this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment);
}
getMetricsCount() {
diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
new file mode 100644
index 00000000000..07a5bda6bcb
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue
@@ -0,0 +1,28 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ name: 'JumpToNextDiscussionButton',
+ components: {
+ icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+};
+</script>
+
+<template>
+ <div class="btn-group" role="group">
+ <button
+ ref="button"
+ v-gl-tooltip
+ class="btn btn-default discussion-next-btn"
+ :title="s__('MergeRequests|Jump to next unresolved discussion')"
+ @click="$emit('onClick')"
+ >
+ <icon name="comment-next" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
new file mode 100644
index 00000000000..2b29d710236
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -0,0 +1,28 @@
+<script>
+export default {
+ name: 'ResolveDiscussionButton',
+ props: {
+ isResolving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ buttonTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')">
+ <i
+ v-if="isResolving"
+ ref="isResolvingIcon"
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin"
+ ></i>
+ {{ buttonTitle }}
+ </button>
+</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 1a116161e3c..695efe3602f 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -12,6 +12,7 @@ import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
+import resolveDiscussionButton from './discussion_resolve_button.vue';
import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
@@ -23,6 +24,7 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
+import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
export default {
name: 'NoteableDiscussion',
@@ -34,6 +36,8 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ resolveDiscussionButton,
+ jumpToNextDiscussionButton,
toggleRepliesWidget,
placeholderNote,
placeholderSystemNote,
@@ -216,6 +220,16 @@ export default {
return null;
},
+ commit() {
+ if (!this.discussion.for_commit) {
+ return null;
+ }
+
+ return {
+ id: this.discussion.commit_id,
+ url: this.discussion.discussion_path,
+ };
+ },
},
watch: {
isReplying() {
@@ -380,6 +394,7 @@ Please check your network connection and try again.`;
:is="componentName(initialDiscussion)"
:note="componentData(initialDiscussion)"
:line="line"
+ :commit="commit"
:help-page-path="helpPagePath"
@handleDeleteNote="deleteNoteHandler"
>
@@ -440,16 +455,12 @@ Please check your network connection and try again.`;
>
Reply...
</button>
- <div v-if="discussion.resolvable">
- <button
- type="button"
- class="btn btn-default ml-sm-2"
- @click="resolveHandler()"
- >
- <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
- {{ resolveButtonTitle }}
- </button>
- </div>
+ <resolve-discussion-button
+ v-if="discussion.resolvable"
+ :is-resolving="isResolving"
+ :button-title="resolveButtonTitle"
+ @onClick="resolveHandler"
+ />
<div
v-if="discussion.resolvable"
class="btn-group discussion-actions ml-sm-2"
@@ -465,16 +476,10 @@ Please check your network connection and try again.`;
<icon name="issue-new" />
</a>
</div>
- <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group">
- <button
- v-gl-tooltip
- class="btn btn-default discussion-next-btn"
- title="Jump to next unresolved discussion"
- @click="jumpToNextDiscussion"
- >
- <icon name="comment-next" />
- </button>
- </div>
+ <jump-to-next-discussion-button
+ v-if="shouldShowJumpToNextDiscussion"
+ @onClick="jumpToNextDiscussion"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 4c02588127e..3c48d81ed05 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,7 +2,9 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import { s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -37,6 +39,11 @@ export default {
required: false,
default: '',
},
+ commit: {
+ type: Object,
+ required: false,
+ default: () => null,
+ },
},
data() {
return {
@@ -73,6 +80,21 @@ export default {
isTarget() {
return this.targetNoteHash === this.noteAnchorId;
},
+ actionText() {
+ if (!this.commit) {
+ return '';
+ }
+
+ // We need to do this to ensure we have the currect sentence order
+ // when translating this as the sentence order may change from one
+ // language to the next. See:
+ // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771
+ const { id, url } = this.commit;
+ const commitLink = `<a class="commit-sha monospace" href="${escape(url)}">${truncateSha(
+ id,
+ )}</a>`;
+ return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false);
+ },
},
created() {
@@ -200,13 +222,10 @@ export default {
</div>
<div class="timeline-content">
<div class="note-header">
- <note-header
- v-once
- :author="author"
- :created-at="note.created_at"
- :note-id="note.id"
- action-text="commented"
- />
+ <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
+ <span v-if="commit" v-html="actionText"></span>
+ <span v-else class="d-none d-sm-inline">&middot;</span>
+ </note-header>
<note-actions
:author-id="author.id"
:note-id="note.id"
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 65f85314fa0..2105a62cecb 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -415,12 +415,13 @@ export const submitSuggestion = (
commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId });
callback();
})
- .catch(() => {
- Flash(
- __('Something went wrong while applying the suggestion. Please try again.'),
- 'alert',
- flashContainer,
+ .catch(err => {
+ const defaultMessage = __(
+ 'Something went wrong while applying the suggestion. Please try again.',
);
+ const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage;
+
+ Flash(__(flashMessage), 'alert', flashContainer);
callback();
});
};
diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js
index 0c585e162cb..8f98be79640 100644
--- a/app/assets/javascripts/pages/explore/projects/index.js
+++ b/app/assets/javascripts/pages/explore/projects/index.js
@@ -1,3 +1,7 @@
import ProjectsList from '~/projects_list';
+import Star from '../../../star';
-document.addEventListener('DOMContentLoaded', () => new ProjectsList());
+document.addEventListener('DOMContentLoaded', () => {
+ new ProjectsList(); // eslint-disable-line no-new
+ new Star('.project-row'); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js
new file mode 100644
index 00000000000..d6afc71fb03
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/tags/releases/index.js
@@ -0,0 +1,8 @@
+import $ from 'jquery';
+import ZenMode from '~/zen_mode';
+import GLForm from '~/gl_form';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ZenMode(); // eslint-disable-line no-new
+ new GLForm($('.release-form')); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index a33835472bb..5ee510eb11d 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,97 +5,101 @@ import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
export default function projectSelect() {
- $('.ajax-project-select').each(function(i, select) {
- var placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- this.groupId = $(select).data('groupId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ this.groupId = $(select).data('groupId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = 'Search for project';
- if (this.includeGroups) {
- placeholder += ' or group';
- }
+ placeholder = 'Search for project';
+ if (this.includeGroups) {
+ placeholder += ' or group';
+ }
- $(select).select2({
- placeholder: placeholder,
- minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
- var data;
- data = {
- results: projects,
- };
- return query.callback(data);
- };
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
+ $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function(_this) {
+ return function(query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
var data;
- data = groups.concat(projects);
- return finalCallback(data);
+ data = {
+ results: projects,
+ };
+ return query.callback(data);
};
- return Api.groups(query.term, {}, groupsCallback);
+ if (_this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(
+ _this.groupId,
+ query.term,
+ {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ with_shared: _this.withShared,
+ include_subgroups: _this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ } else {
+ return Api.projects(
+ query.term,
+ {
+ order_by: _this.orderBy,
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ membership: !_this.allProjects,
+ },
+ projectsCallback,
+ );
+ }
};
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(
- _this.groupId,
- query.term,
- {
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- with_shared: _this.withShared,
- include_subgroups: _this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
- } else {
- return Api.projects(
- query.term,
- {
- order_by: _this.orderBy,
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- membership: !_this.allProjects,
- },
- projectsCallback,
- );
- }
- };
- })(this),
- id: function(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text: function(project) {
- return project.name_with_namespace || project.name;
- },
+ })(this),
+ id: function(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text: function(project) {
+ return project.name_with_namespace || project.name;
+ },
- initSelection: function(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection: function(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 3dbac3ff942..d3b5f532dc1 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -44,9 +44,13 @@ export default class ProjectSelectComboButton {
// eslint-disable-next-line class-methods-use-this
openDropdown(event) {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
}
selectProject() {
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index ce051582299..4017630d6ef 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -579,101 +579,109 @@ function UsersSelect(currentUser, els, options = {}) {
};
})(this),
);
- $('.ajax-users-select').each(
- (function(_this) {
- return function(i, select) {
- var firstUser, showAnyUser, showEmailUser, showNullUser;
- var options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('projectId');
- options.groupId = $(select).data('groupId');
- options.showCurrentUser = $(select).data('currentUser');
- options.authorId = $(select).data('authorId');
- options.skipUsers = $(select).data('skipUsers');
- showNullUser = $(select).data('nullUser');
- showAnyUser = $(select).data('anyUser');
- showEmailUser = $(select).data('emailUser');
- firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: 'Search for a user',
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query: function(query) {
- return _this.users(query.term, options, function(users) {
- var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
- data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- ref = data.results;
-
- for (index = 0, len = ref.length; index < len; index += 1) {
- obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each(
+ (function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('projectId');
+ options.groupId = $(select).data('groupId');
+ options.showCurrentUser = $(select).data('currentUser');
+ options.authorId = $(select).data('authorId');
+ options.skipUsers = $(select).data('skipUsers');
+ showNullUser = $(select).data('nullUser');
+ showAnyUser = $(select).data('anyUser');
+ showEmailUser = $(select).data('emailUser');
+ firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: 'Search for a user',
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, len, name, nullUser, obj, ref;
+ data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ ref = data.results;
+
+ for (index = 0, len = ref.length; index < len; index += 1) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ name: name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- nullUser = {
- name: 'Unassigned',
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = 'Any User';
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ var trimmed = query.term.trim();
+ emailUser = {
+ name: 'Invite "' + trimmed + '" by email',
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- anyUser = {
- name: name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- var trimmed = query.term.trim();
- emailUser = {
- name: 'Invite "' + trimmed + '" by email',
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup: function(m) {
+ return m;
+ },
});
- },
- initSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.initSelection.apply(_this, args);
- },
- formatResult: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
- },
- formatSelection: function() {
- var args;
- args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup: function(m) {
- return m;
- },
- });
- };
- })(this),
- );
+ };
+ })(this),
+ );
+ })
+ .catch(() => {});
}
UsersSelect.prototype.initSelection = function(element, callback) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
deleted file mode 100644
index 8780aa4bd1c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetOptions from './mr_widget_options.vue';
-
-export default MRWidgetOptions;
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 60cebbfc2b2..0cedbdbdfef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import MrWidgetOptions from './ee_switch_mr_widget_options';
+import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
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 5a9d86594b1..0ce9d271845 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
@@ -3,6 +3,9 @@ import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
+import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
+import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
@@ -28,10 +31,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue';
-import MRWidgetStore from './stores/ee_switch_mr_widget_store';
-import MRWidgetService from './services/ee_switch_mr_widget_service';
import eventHub from './event_hub';
-import stateMaps from './stores/ee_switch_state_maps';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
deleted file mode 100644
index ea2aabb78fe..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MRWidgetService from './mr_widget_service';
-
-export default MRWidgetService;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
deleted file mode 100644
index ebef30e3eab..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import getStateKey from './get_state_key';
-
-export default getStateKey;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
deleted file mode 100644
index 92a07c53f2d..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MergeRequestStore from './mr_widget_store';
-
-export default MergeRequestStore;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
deleted file mode 100644
index 50cf9503ea7..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import stateMaps from './state_maps';
-
-export default stateMaps;
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 e5a52c6a7f6..ab194e84ab4 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
@@ -1,5 +1,5 @@
import Timeago from 'timeago.js';
-import getStateKey from './ee_switch_get_state_key';
+import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index b9f884074d0..a351ca62c94 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -42,7 +42,7 @@ export default {
</script>
<template>
- <div>
+ <div class="md-suggestion">
<suggestion-diff-header
class="qa-suggestion-diff-header"
:can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index dcda701f049..c33665c24f6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -130,6 +130,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
- <div v-show="isRendered" ref="container" v-html="noteHtml"></div>
+ <div v-show="isRendered" ref="container" class="note-text md" v-html="noteHtml"></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 95f4395ac13..a6c1737dcab 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -68,7 +68,8 @@ export default {
sanitizedSource() {
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
// Only adds the width to the URL if its not a base64 data image
- if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`;
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.size}`;
return baseSrc;
},
resultantSrcAttribute() {
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index d24fe1b547e..f9773622001 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -28,10 +28,10 @@ export default {
},
computed: {
statusHtml() {
- if (this.user.status.emoji && this.user.status.message) {
- return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
- } else if (this.user.status.message) {
- return this.user.status.message;
+ if (this.user.status.emoji && this.user.status.message_html) {
+ return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
+ } else if (this.user.status.message_html) {
+ return this.user.status.message_html;
}
return '';
},
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 5d2cbdde8dc..d164cc56e44 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -42,6 +42,10 @@
color: $text;
border-color: $border;
+ &.btn-border-color {
+ border-color: $border-color;
+ }
+
> .icon {
color: $text;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a499a3a9f95..0fb9bde1785 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -381,6 +381,7 @@ img.emoji {
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
+.vertical-align-sub { vertical-align: sub; }
.flex-align-self-center { align-self: center; }
.flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; }
@@ -408,3 +409,14 @@ img.emoji {
.gl-pr-3 { padding-right: #{2 * $grid-size}; }
.gl-pr-4 { padding-right: #{3 * $grid-size}; }
.gl-pr-5 { padding-right: #{4 * $grid-size}; }
+
+/**
+ * Removes browser specific clear icon from input fields in
+ * Internet Explorer 10, Internet Explorer 11, and Microsoft Edge.
+ * This is intended for elements which add a customized clear icon.
+ *
+ * see also https://developer.mozilla.org/en-US/docs/Web/CSS/::-ms-clear
+ */
+.ms-no-clear ::-ms-clear {
+ display: none;
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 679148ddf7b..f708a26bb32 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -280,6 +280,8 @@
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
+ width: 100% !important;
+ font-family: $monospace-font !important;
}
.md-suggestion-header {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e886a54dc99..9eae9a831fa 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -278,8 +278,8 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
-$project-title-row-height: 64px;
-$project-avatar-mobile-size: 24px;
+$home-panel-title-row-height: 64px;
+$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
diff --git a/app/assets/stylesheets/highlight/none.scss b/app/assets/stylesheets/highlight/none.scss
index 7d692a87e33..7ced4e82e66 100644
--- a/app/assets/stylesheets/highlight/none.scss
+++ b/app/assets/stylesheets/highlight/none.scss
@@ -38,7 +38,7 @@
$none-over-bg: #ded7fc;
$none-expanded-border: #e0e0e0;
- $none-expanded-bg: #f7f7f7;
+ $none-expanded-bg: #e0e0e0;
.line_holder {
@@ -50,18 +50,12 @@
.diff-line-num {
&.old {
- background-color: $line-number-old;
- border-color: $line-removed-dark;
-
a {
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
}
}
&.new {
- background-color: $line-number-new;
- border-color: $line-added-dark;
-
a {
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
}
@@ -78,8 +72,8 @@
}
&.hll:not(.empty-cell) {
- background-color: $line-number-select;
- border-color: $line-select-yellow-dark;
+ background-color: $white-light;
+ border-color: $white-normal;
}
}
@@ -101,26 +95,28 @@
.line_content {
&.old {
- background-color: $line-removed;
+ background-color: $white-normal;
&::before {
- color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ color: $gl-text-color;
}
span.idiff {
- background-color: $line-removed-dark;
+ background-color: $white-normal;
+ text-decoration: underline;
}
}
&.new {
- background-color: $line-added;
+ background-color: $white-normal;
&::before {
- color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ color: $gl-text-color;
}
span.idiff {
- background-color: $line-added-dark;
+ background-color: $white-normal;
+ text-decoration: underline;
}
}
@@ -129,7 +125,7 @@
}
&.hll:not(.empty-cell) {
- background-color: $line-select-yellow;
+ background-color: $white-normal;
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 553cc44fe83..1f24b8dfa9e 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -395,6 +395,11 @@ $ide-commit-header-height: 48px;
svg {
vertical-align: sub;
}
+
+ .ide-status-avatar {
+ float: none;
+ margin: 0 0 1px;
+ }
}
.ide-status-file {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 4f804866886..02aac58a475 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1029,7 +1029,7 @@
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
- max-height: calc(100vh - $top-pos);
+ max-height: calc(100vh - #{$top-pos});
padding-right: $gl-padding;
.file-row {
@@ -1040,7 +1040,7 @@
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
- max-height: calc(100vh - $performance-bar-top-pos);
+ max-height: calc(100vh - #{$performance-bar-top-pos});
}
}
@@ -1095,12 +1095,6 @@
}
}
-.tree-list-view-toggle {
- svg {
- top: 0;
- }
-}
-
.image-diff-overlay,
.image-diff-overlay-add-comment {
top: 0;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ebbb5beed81..8ade995525a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -29,9 +29,7 @@
}
}
-.group-nav-container .group-search,
.group-nav-container .nav-controls {
- display: flex;
align-items: flex-start;
padding: $gl-padding-top 0 0;
@@ -44,6 +42,52 @@
margin-top: 0;
}
+ @include media-breakpoint-down(sm) {
+ .dropdown,
+ .dropdown .dropdown-toggle,
+ .btn-success {
+ display: block;
+ }
+
+ .group-filter-form,
+ .dropdown {
+ margin-bottom: 10px;
+ margin-right: 0;
+ }
+
+ &,
+ .group-filter-form,
+ .group-filter-form-field,
+ .dropdown,
+ .dropdown .dropdown-toggle,
+ .btn-success {
+ width: 100%;
+ }
+
+ .dropdown .dropdown-toggle .fa-chevron-down {
+ position: absolute;
+ top: 11px;
+ right: 8px;
+ }
+ }
+}
+
+.home-panel-buttons {
+ .home-panel-action-button {
+ vertical-align: top;
+ }
+
+
+ .notification-dropdown {
+ .dropdown-menu {
+ @extend .dropdown-menu-right;
+ }
+
+ .icon {
+ fill: $gl-text-color-secondary;
+ }
+ }
+
.new-project-subgroup {
.dropdown-primary {
min-width: 115px;
@@ -99,61 +143,29 @@
font-weight: $gl-font-weight-bold;
}
}
- }
- }
-
- @include media-breakpoint-down(sm) {
- &,
- .dropdown,
- .dropdown .dropdown-toggle,
- .btn-success {
- display: block;
- }
- .group-filter-form,
- .dropdown {
- margin-bottom: 10px;
- margin-right: 0;
- }
-
- .group-filter-form,
- .dropdown .dropdown-toggle,
- .btn-success {
- width: 100%;
- }
-
- .dropdown .dropdown-toggle .fa-chevron-down {
- position: absolute;
- top: 11px;
- right: 8px;
- }
-
- .new-project-subgroup {
- display: flex;
- align-items: flex-start;
+ @include media-breakpoint-down(sm) {
+ display: flex;
+ align-items: flex-start;
- .dropdown-primary {
- flex: 1;
- }
+ .dropdown-primary {
+ flex: 1;
+ }
- .dropdown-toggle {
- width: auto;
- }
+ .dropdown-toggle {
+ width: auto;
+ }
- .dropdown-menu {
- width: 100%;
- max-width: inherit;
- min-width: inherit;
+ .dropdown-menu {
+ width: 100%;
+ max-width: inherit;
+ min-width: inherit;
+ }
}
}
}
}
-.group-nav-container .group-search {
- padding: $gl-padding 0;
- border-bottom: 1px solid $border-color;
-}
-
.groups-listing {
.group-list-tree .group-row:first-child {
border-top: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index a28921592ec..e676d48c1f4 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -861,7 +861,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
- top: 0;
+ top: 1px;
vertical-align: initial;
}
}
@@ -869,7 +869,7 @@ button.mini-pipeline-graph-dropdown-toggle {
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
- top: 2px;
+ top: 4px;
}
.scrollable-menu {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 505f6e036e3..2342c284a5e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -140,73 +140,19 @@
}
}
-.project-home-panel,
-.group-home-panel {
- padding-top: 24px;
- padding-bottom: 24px;
-
- .group-avatar {
- float: none;
- margin: 0 auto;
-
- &.identicon {
- border-radius: 50%;
- }
- }
-
- .group-title {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 24px;
- font-weight: $gl-font-weight-normal;
- line-height: 1;
- word-wrap: break-word;
-
- .fa {
- margin-left: 2px;
- font-size: 12px;
- vertical-align: middle;
- }
- }
-
- .group-home-desc {
- margin-left: auto;
- margin-right: auto;
- margin-bottom: 0;
- max-width: 700px;
-
- > p {
- margin-bottom: 0;
- }
- }
-
- .notifications-btn {
- .fa-bell,
- .fa-spinner {
- margin-right: 6px;
- }
-
- .fa-angle-down {
- margin-left: 6px;
- }
- }
-}
-
+.group-home-panel,
.project-home-panel {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
- .project-avatar {
- width: $project-title-row-height;
- height: $project-title-row-height;
+ .home-panel-avatar {
+ width: $home-panel-title-row-height;
+ height: $home-panel-title-row-height;
flex-shrink: 0;
- flex-basis: $project-title-row-height;
- margin: 0 $gl-padding 0 0;
+ flex-basis: $home-panel-title-row-height;
}
- .project-title {
- margin-top: 8px;
- margin-bottom: 5px;
+ .home-panel-title {
font-size: 20px;
line-height: $gl-line-height-24;
font-weight: bold;
@@ -215,11 +161,7 @@
font-size: $gl-font-size-large;
}
- .project-visibility {
- color: $gl-text-color-secondary;
- }
-
- .project-topic-list {
+ .home-panel-topic-list {
font-size: $gl-font-size;
font-weight: $gl-font-weight-normal;
@@ -231,12 +173,12 @@
}
}
- .project-title-row {
+ .home-panel-title-row {
@include media-breakpoint-down(sm) {
- .project-avatar {
- width: $project-avatar-mobile-size;
- height: $project-avatar-mobile-size;
- flex-basis: $project-avatar-mobile-size;
+ .home-panel-avatar {
+ width: $home-panel-avatar-mobile-size;
+ height: $home-panel-avatar-mobile-size;
+ flex-basis: $home-panel-avatar-mobile-size;
.avatar {
font-size: 20px;
@@ -244,28 +186,26 @@
}
}
- .project-title {
+ .home-panel-title {
margin-top: 4px;
margin-bottom: 2px;
font-size: $gl-font-size;
line-height: $gl-font-size-large;
}
- .project-topic-list,
- .project-metadata {
+ .home-panel-topic-list,
+ .home-panel-metadata {
font-size: $gl-font-size-small;
}
}
}
- .project-metadata {
+ .home-panel-metadata {
font-weight: normal;
font-size: 14px;
line-height: $gl-btn-line-height;
- color: $gl-text-color-secondary;
-
- .project-license {
+ .home-panel-license {
.btn {
line-height: 0;
border-width: 0;
@@ -273,13 +213,13 @@
}
.access-request-link,
- .project-topic-list {
+ .home-panel-topic-list {
padding-left: $gl-padding-8;
border-left: 1px solid $gl-text-color-secondary;
}
}
- .project-description {
+ .home-panel-description {
@include media-breakpoint-up(md) {
font-size: $gl-font-size-large;
}
@@ -292,12 +232,11 @@
}
}
-.nav > .project-repo-buttons {
+.nav > .project-buttons {
margin-top: 0;
}
-.project-repo-buttons,
-.group-buttons {
+.project-repo-buttons {
.btn {
&:last-child {
margin-left: 0;
@@ -318,8 +257,30 @@
margin-left: 0;
}
}
+
+ .notifications-icon {
+ top: 1px;
+ margin-right: 0;
+ }
}
+ .icon {
+ top: 0;
+ }
+
+ .count-badge,
+ .btn-xs {
+ height: 24px;
+ }
+
+ .dropdown-toggle,
+ .clone-dropdown-btn {
+ .fa {
+ color: unset;
+ }
+ }
+
+ .home-panel-action-button,
.project-action-button {
margin: $gl-padding $gl-padding-8 0 0;
vertical-align: top;
@@ -385,31 +346,6 @@
}
}
-.project-repo-buttons {
- .icon {
- top: 0;
- }
-
- .count-badge,
- .btn-xs {
- height: 24px;
- }
-
- .dropdown-toggle,
- .clone-dropdown-btn {
- .fa {
- color: unset;
- }
- }
-
- .btn {
- .notifications-icon {
- top: 1px;
- margin-right: 0;
- }
- }
-}
-
.split-one {
display: inline-table;
margin-right: 12px;
@@ -772,9 +708,6 @@
.project-stats,
.project-buttons {
- font-size: 0;
- text-align: center;
-
.scrolling-tabs-container {
.scrolling-tabs {
margin-top: $gl-padding-8;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c5b9d1f6885..811cc310a8f 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -167,12 +167,14 @@
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
+ vertical-align: top;
}
.option-description,
.option-disabled-reason {
margin-left: 30px;
color: $project-option-descr-color;
+ margin-top: -5px;
}
.option-disabled-reason {
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 789e0dc736e..07d0bf16d93 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -129,13 +129,13 @@ module IssuableCollections
return sort_param if Gitlab::Database.read_only?
if user_preference[issuable_sorting_field] != sort_param
- user_preference.update_attribute(issuable_sorting_field, sort_param)
+ user_preference.update(issuable_sorting_field => sort_param)
end
sort_param
end
- # Implement default_sorting_field method on controllers
+ # Implement issuable_sorting_field method on controllers
# to choose which column to store the sorting parameter.
def issuable_sorting_field
nil
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index a75590457d6..18ed4027eac 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module IssuesAction
+module IssuableCollectionsAction
extend ActiveSupport::Concern
include IssuableCollections
include IssuesCalendar
@@ -18,6 +18,12 @@ module IssuesAction
format.atom { render layout: 'xml.atom' }
end
end
+
+ def merge_requests
+ @merge_requests = issuables_collection.page(params[:page])
+
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
+ end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def issues_calendar
@@ -26,8 +32,29 @@ module IssuesAction
private
+ def issuable_sorting_field
+ case action_name
+ when 'issues'
+ Issue::SORTING_PREFERENCE_FIELD
+ when 'merge_requests'
+ MergeRequest::SORTING_PREFERENCE_FIELD
+ else
+ nil
+ end
+ end
+
def finder_type
- (super if defined?(super)) ||
- (IssuesFinder if %w(issues issues_calendar).include?(action_name))
+ case action_name
+ when 'issues', 'issues_calendar'
+ IssuesFinder
+ when 'merge_requests'
+ MergeRequestsFinder
+ else
+ nil
+ end
+ end
+
+ def finder_options
+ super.merge(non_archived: true)
end
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index ca713192c9e..6402e01ddc0 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -35,7 +35,9 @@ module MembershipActions
respond_to do |format|
format.html do
- message = "User was successfully removed from #{source_type}."
+ source = source_type == 'group' ? 'group and any subresources' : source_type
+
+ message = "User was successfully removed from #{source}."
redirect_to members_page_url, notice: message
end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
deleted file mode 100644
index ed10f32512e..00000000000
--- a/app/controllers/concerns/merge_requests_action.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequestsAction
- extend ActiveSupport::Concern
- include IssuableCollections
-
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def merge_requests
- @merge_requests = issuables_collection.page(params[:page])
-
- @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
- end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
-
- private
-
- def finder_type
- (super if defined?(super)) ||
- (MergeRequestsFinder if action_name == 'merge_requests')
- end
-
- def finder_options
- super.merge(non_archived: true)
- end
-end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 3802aa5f40f..9484e4d30cd 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -27,7 +27,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def group_milestones
groups = GroupsFinder.new(current_user, all_available: false).execute
- DashboardGroupMilestone.build_collection(groups)
+ DashboardGroupMilestone.build_collection(groups, params)
end
# See [#39545](https://gitlab.com/gitlab-org/gitlab-ce/issues/39545) for info about the deprecation of dynamic milestones
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index be2d9512c01..75329b05a6f 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
class DashboardController < Dashboard::ApplicationController
- include IssuesAction
- include MergeRequestsAction
+ include IssuableCollectionsAction
prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 9f074690cbc..f3d76c5a478 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -15,7 +15,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
@@ -30,7 +30,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
@@ -44,7 +44,7 @@ class Explore::ProjectsController < Explore::ApplicationController
format.html
format.json do
render json: {
- html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
+ html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects })
}
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 868deea3f01..7ed4384089b 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -115,6 +115,6 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def search_params
- params.permit(:state).merge(group_ids: group.id)
+ params.permit(:state, :search_title).merge(group_ids: group.id)
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index c5d8ac2ed77..15aadf3f74b 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -2,8 +2,7 @@
class GroupsController < Groups::ApplicationController
include API::Helpers::RelatedResourcesHelpers
- include IssuesAction
- include MergeRequestsAction
+ include IssuableCollectionsAction
include ParamsBackwardCompatibility
include PreviewMarkdown
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 1b30b4dda36..2b1395f364f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
- response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
+ response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
@@ -89,7 +89,7 @@ class Import::BitbucketController < Import::BaseController
end
def go_to_bitbucket_for_permissions
- redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
+ redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end
def bitbucket_unauthorized
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 87338488eba..f333e43b892 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -13,7 +13,10 @@ class Import::BitbucketServerController < Import::BaseController
# Repository names are limited to 128 characters. They must start with a
# letter or number and may contain spaces, hyphens, underscores, and periods.
# (https://community.atlassian.com/t5/Answers-Developer-Questions/stash-repository-names/qaq-p/499054)
- VALID_BITBUCKET_CHARS = /\A[\w\-_\.\s]+\z/
+ #
+ # Bitbucket Server starts personal project names with a tilde.
+ VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/
+ VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/
def new
end
@@ -91,7 +94,7 @@ class Import::BitbucketServerController < Import::BaseController
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
return render_validation_error('Missing repository slug') unless @repo_slug.present?
- return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_CHARS
+ return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_PROJECT_CHARS
return render_validation_error('Invalid repository slug') unless @repo_slug =~ VALID_BITBUCKET_CHARS
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 34c7dbdc2fe..3fbc0817e95 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -83,7 +83,7 @@ class Import::GithubController < Import::BaseController
end
def callback_import_url
- public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
+ public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 384f308269a..43c4f4d220e 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -17,7 +17,8 @@ class NotificationSettingsController < ApplicationController
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
if params[:hide_label].present?
- render_response("projects/buttons/_notifications")
+ btn_class = params[:project_id].present? ? 'btn-xs' : ''
+ render_response("shared/notifications/_new_button", btn_class)
else
render_response
end
@@ -41,9 +42,9 @@ class NotificationSettingsController < ApplicationController
can?(current_user, ability_name, resource)
end
- def render_response(response_template = "shared/notifications/_button")
+ def render_response(response_template = "shared/notifications/_button", btn_class = nil)
render json: {
- html: view_to_html_string(response_template, notification_setting: @notification_setting),
+ html: view_to_html_string(response_template, notification_setting: @notification_setting, btn_class: btn_class),
saved: @saved
}
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index a63eea0ca0e..1a1b024d766 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:area_chart, project)
end
+ # Returns all environments or all folders based on the :nested param
def index
@environments = project.environments
.with_state(params[:scope] || :available)
@@ -25,11 +26,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .within_folders
- .represent(@environments),
+ environments: serialize_environments(request, response, params[:nested]),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
@@ -37,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ # Returns all environments for a given folder
# rubocop: disable CodeReuse/ActiveRecord
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@@ -48,10 +46,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.html
format.json do
render json: {
- environments: EnvironmentSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .represent(@environments),
+ environments: serialize_environments(request, response),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
@@ -186,6 +181,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment ||= project.environments.find(params[:id])
end
+ def serialize_environments(request, response, nested = false)
+ serializer = EnvironmentSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ serializer = serializer.within_folders if nested
+ serializer.represent(@environments)
+ end
+
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e3e60665506..69f983f7ccd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -19,7 +19,7 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
- prepend_before_action :authenticate_new_issue!, only: [:new]
+ prepend_before_action :authenticate_user!, only: [:new]
prepend_before_action :store_uri, only: [:new, :show]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
@@ -191,6 +191,10 @@ class Projects::IssuesController < Projects::ApplicationController
protected
+ def issuable_sorting_field
+ Issue::SORTING_PREFERENCE_FIELD
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def issue
return @issue if defined?(@issue)
@@ -245,14 +249,6 @@ class Projects::IssuesController < Projects::ApplicationController
] + [{ label_ids: [], assignee_ids: [] }]
end
- def authenticate_new_issue!
- return if current_user
-
- notice = "Please sign in to create the new issue."
-
- redirect_to new_user_session_path, notice: notice
- end
-
def store_uri
if request.get? && !request.xhr?
store_location_for :user, request.fullpath
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index babeee48ef3..013e01b82aa 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -5,7 +5,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
include WorkhorseRequest
include SendFileUpload
- skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
+ skip_before_action :verify_workhorse_api!, only: :download
def download
lfs_object = LfsObject.find_by_oid(oid)
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 368ee89ff5c..54ff7ded8e5 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -39,8 +39,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
def set_pipeline_variables
- @pipelines = @merge_request.all_pipelines
- @pipeline = @merge_request.head_pipeline
- @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
+ @pipelines =
+ if can?(current_user, :read_pipeline, @project)
+ @merge_request.all_pipelines
+ else
+ Ci::Pipeline.none
+ end
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index accc37557b0..bc0a3d3526d 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -230,6 +230,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
+ def issuable_sorting_field
+ MergeRequest::SORTING_PREFERENCE_FIELD
+ end
+
def merge_params
params.permit(merge_params_attributes)
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 8bc59d8a305..f6f61b6e5fb 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -147,6 +147,6 @@ class Projects::MilestonesController < Projects::ApplicationController
groups = project_group.self_and_ancestors.select(:id)
end
- params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
+ params.permit(:state, :search_title).merge(project_ids: @project.id, group_ids: groups)
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 67827b1d3bb..6a86f8ca729 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -4,6 +4,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :authorize_read_pipeline!
+ before_action :authorize_read_build!, only: [:index]
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
@@ -69,7 +70,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: PipelineSerializer
.new(project: @project, current_user: @current_user)
- .represent(@pipeline, grouped: true)
+ .represent(@pipeline, show_represent_params)
end
end
end
@@ -157,6 +158,10 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def show_represent_params
+ { grouped: true }
+ end
+
def create_params
params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 75e590f3f33..f2f63e986bb 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -99,7 +99,9 @@ module Projects
def define_triggers_variables
@triggers = @project.triggers
+ .present(current_user: current_user)
@trigger = ::Ci::Trigger.new
+ .present(current_user: current_user)
end
def define_badges_variables
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index f5fdfb8accc..c7b4ebb2b24 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController
end
def trigger
- @trigger ||= project.triggers.find(params[:id]) || render_404
+ @trigger ||= project.triggers.find(params[:id])
+ .present(current_user: current_user)
end
def trigger_params
- params.require(:trigger).permit(
- :description
- )
+ params.require(:trigger).permit(:description)
end
end
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
index c1ef9dfefa7..f8c7f0c3167 100644
--- a/app/finders/contributed_projects_finder.rb
+++ b/app/finders/contributed_projects_finder.rb
@@ -14,6 +14,9 @@ class ContributedProjectsFinder < UnionFinder
# Returns an ActiveRecord::Relation.
# rubocop: disable CodeReuse/ActiveRecord
def execute(current_user = nil)
+ # Do not show contributed projects if the user profile is private.
+ return Project.none unless can_read_profile?(current_user)
+
segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_id_desc
@@ -22,6 +25,10 @@ class ContributedProjectsFinder < UnionFinder
private
+ def can_read_profile?(current_user)
+ Ability.allowed?(current_user, :read_user_profile, @user)
+ end
+
def all_projects(current_user)
projects = []
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index fcd54b6106e..77b55cbb838 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -22,6 +22,7 @@ class MilestonesFinder
items = Milestone.all
items = by_groups_and_projects(items)
items = by_title(items)
+ items = by_search_title(items)
items = by_state(items)
order(items)
@@ -43,6 +44,14 @@ class MilestonesFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_search_title(items)
+ if params[:search_title].present?
+ items.search_title(params[:search_title])
+ else
+ items
+ end
+ end
+
def by_state(items)
Milestone.filter_by_state(items, params[:state])
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index fa5d3ae474a..dedc58f482b 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -36,6 +36,14 @@ module EmailsHelper
nil
end
+ def sanitize_name(name)
+ if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF]
+ name.tr('.', '_')
+ else
+ name
+ end
+ end
+
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
deleted file mode 100644
index e36d63b2946..00000000000
--- a/app/helpers/external_wiki_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module ExternalWikiHelper
- def get_project_wiki_path(project)
- external_wiki_service = project.external_wiki
- if external_wiki_service
- external_wiki_service.properties['external_wiki_url']
- else
- project_wiki_path(project, :home)
- end
- end
-end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 49171df1433..d3befd87ccc 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -8,7 +8,9 @@ module ImportHelper
end
def sanitize_project_name(name)
- name.gsub(/[^\w\-]/, '-')
+ # For personal projects in Bitbucket in the form ~username, we can
+ # just drop that leading tilde.
+ name.gsub(/\A~+/, '').gsub(/[^\w\-]/, '-')
end
def import_project_target(owner, name)
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index ab4a1ccc0d1..11d5591d509 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -18,12 +18,13 @@ module MembersHelper
"remove #{member.user.name} from"
end
- "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end
def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user'
- "#{action} from #{member.real_source_type.humanize(capitalize: false)}"
+
+ "#{action} from #{source_text(member)}"
end
def leave_confirmation_message(member_source)
@@ -35,4 +36,14 @@ module MembersHelper
options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}"
end
+
+ private
+
+ def source_text(member)
+ type = member.real_source_type.humanize(capitalize: false)
+
+ return type if member.request? || member.invite? || type != 'group'
+
+ 'group and any subresources'
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 033686823a2..293dd20ad49 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -85,7 +85,7 @@ module NotesHelper
diffs_project_merge_request_path(discussion.project, discussion.noteable, path_params)
elsif discussion.for_commit?
- anchor = discussion.line_code if discussion.diff_discussion?
+ anchor = discussion.diff_discussion? ? discussion.line_code : "note_#{discussion.first_note.id}"
project_commit_path(discussion.project, discussion.noteable, anchor: anchor)
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eceee054ede..85248a16f50 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -305,7 +305,8 @@ module ProjectsHelper
nav_tabs << :container_registry
end
- if project.builds_enabled? && can?(current_user, :read_pipeline, project)
+ # Pipelines feature is tied to presence of builds
+ if can?(current_user, :read_build, project)
nav_tabs << :pipelines
end
@@ -313,19 +314,24 @@ module ProjectsHelper
nav_tabs << :operations
end
- if project.external_issue_tracker
- nav_tabs << :external_issue_tracker
- end
-
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
+ nav_tabs << external_nav_tabs(project)
+
nav_tabs.flatten
end
+ def external_nav_tabs(project)
+ [].tap do |tabs|
+ tabs << :external_issue_tracker if project.external_issue_tracker
+ tabs << :external_wiki if project.has_external_wiki?
+ end
+ end
+
def tab_ability_map
{
environments: :read_environment,
diff --git a/app/helpers/release_blog_post_helper.rb b/app/helpers/release_blog_post_helper.rb
deleted file mode 100644
index 31b5b7edc39..00000000000
--- a/app/helpers/release_blog_post_helper.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module ReleaseBlogPostHelper
- def blog_post_url
- Gitlab::ReleaseBlogPost.instance.blog_post_url
- end
-end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 71fbba5b328..29696ab276f 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -2,4 +2,8 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
+
+ def self.id_in(ids)
+ where(id: ids)
+ end
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 29aa00a66d9..5450d40ea95 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -2,11 +2,13 @@
module Ci
class Bridge < CommitStatus
+ include Ci::Processable
include Importable
include AfterCommitQueue
include Gitlab::Utils::StrongMemoize
belongs_to :project
+ belongs_to :trigger_request
validates :ref, presence: true
def self.retry(bridge, current_user)
@@ -23,6 +25,21 @@ module Ci
.fabricate!
end
+ def schedulable?
+ false
+ end
+
+ def action?
+ false
+ end
+
+ def artifacts?
+ false
+ end
+
+ def expanded_environment_name
+ end
+
def predefined_variables
raise NotImplementedError
end
@@ -30,5 +47,9 @@ module Ci
def execute_hooks
raise NotImplementedError
end
+
+ def to_partial_path
+ 'projects/generic_commit_statuses/generic_commit_status'
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index cfdb3c0d719..84010e40ef4 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,6 +3,8 @@
module Ci
class Build < CommitStatus
prepend ArtifactMigratable
+ include Ci::Processable
+ include Ci::Metadatable
include TokenAuthenticatable
include AfterCommitQueue
include ObjectStorage::BackgroundMove
@@ -36,12 +38,10 @@ module Ci
has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
end
- has_one :metadata, class_name: 'Ci::BuildMetadata', autosave: true
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
- delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
@@ -132,7 +132,6 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
- before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -260,10 +259,6 @@ module Ci
end
end
- def ensure_metadata
- metadata || build_metadata(project: project)
- end
-
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
@@ -283,18 +278,6 @@ module Ci
self.name == 'pages'
end
- # degenerated build is one that cannot be run by Runner
- def degenerated?
- self.options.blank?
- end
-
- def degenerate!
- Build.transaction do
- self.update!(options: nil, yaml_variables: nil)
- self.metadata&.destroy
- end
- end
-
def archived?
return true if degenerated?
@@ -638,26 +621,6 @@ module Ci
super || project.try(:build_coverage_regex)
end
- def when
- read_attribute(:when) || 'on_success'
- end
-
- def options
- read_metadata_attribute(:options, :config_options, {})
- end
-
- def yaml_variables
- read_metadata_attribute(:yaml_variables, :config_variables, [])
- end
-
- def options=(value)
- write_metadata_attribute(:options, :config_options, value)
- end
-
- def yaml_variables=(value)
- write_metadata_attribute(:yaml_variables, :config_variables, value)
- end
-
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables if user.blank?
@@ -959,20 +922,5 @@ module Ci
def project_destroyed?
project.pending_delete?
end
-
- def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
- read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value
- end
-
- def write_metadata_attribute(legacy_key, metadata_key, value)
- # save to metadata or this model depending on the state of feature flag
- if Feature.enabled?(:ci_build_metadata_config)
- ensure_metadata.write_attribute(metadata_key, value)
- write_attribute(legacy_key, nil)
- else
- write_attribute(legacy_key, value)
- metadata&.write_attribute(metadata_key, nil)
- end
- end
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 38390f49217..cd8eb774cf5 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -10,7 +10,7 @@ module Ci
self.table_name = 'ci_builds_metadata'
- belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
before_create :set_build_project
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 30a957b4117..acef5d2e643 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -25,6 +25,8 @@ module Ci
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
+ has_many :processables, -> { processables },
+ class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 58f3fe2460a..0389945191e 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -14,6 +14,7 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
+ has_many :bridges, foreign_key: :stage_id
with_options unless: :importing? do
validates :project, presence: true
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 55db42162ca..637148c4ce4 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,6 +4,7 @@ module Ci
class Trigger < ActiveRecord::Base
extend Gitlab::Ci::Model
include IgnorableColumn
+ include Presentable
ignore_column :deleted_at
@@ -29,7 +30,7 @@ module Ci
end
def short_token
- token[0...4]
+ token[0...4] if token.present?
end
def legacy?
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 01f4c58daa1..982e13e2845 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -11,6 +11,7 @@ class Commit
include Mentionable
include Referable
include StaticModel
+ include Presentable
include ::Gitlab::Utils::StrongMemoize
attr_mentionable :safe_message, pipeline: :single_line
@@ -304,7 +305,9 @@ class Commit
end
def last_pipeline
- @last_pipeline ||= pipelines.last
+ strong_memoize(:last_pipeline) do
+ pipelines.last
+ end
end
def status(ref = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 0f50bd39131..7f6562b63e5 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -41,6 +41,7 @@ class CommitStatus < ActiveRecord::Base
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
+ scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) }
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 73a27326f6c..002f3e17891 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -15,7 +15,7 @@ module CacheMarkdownField
# Increment this number every time the renderer changes its output
CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 13
+ CACHE_COMMONMARK_VERSION = 14
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
new file mode 100644
index 00000000000..9eed9492b37
--- /dev/null
+++ b/app/models/concerns/ci/metadatable.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that need to read and write
+ # metadata for CI/CD entities.
+ #
+ module Metadatable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :metadata, class_name: 'Ci::BuildMetadata',
+ foreign_key: :build_id,
+ inverse_of: :build,
+ autosave: true
+
+ delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ before_create :ensure_metadata
+ end
+
+ def ensure_metadata
+ metadata || build_metadata(project: project)
+ end
+
+ def degenerated?
+ self.options.blank?
+ end
+
+ def degenerate!
+ self.class.transaction do
+ self.update!(options: nil, yaml_variables: nil)
+ self.metadata&.destroy
+ end
+ end
+
+ def options
+ read_metadata_attribute(:options, :config_options, {})
+ end
+
+ def yaml_variables
+ read_metadata_attribute(:yaml_variables, :config_variables, [])
+ end
+
+ def options=(value)
+ write_metadata_attribute(:options, :config_options, value)
+ end
+
+ def yaml_variables=(value)
+ write_metadata_attribute(:yaml_variables, :config_variables, value)
+ end
+
+ private
+
+ def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
+ read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value
+ end
+
+ def write_metadata_attribute(legacy_key, metadata_key, value)
+ # save to metadata or this model depending on the state of feature flag
+ if Feature.enabled?(:ci_build_metadata_config)
+ ensure_metadata.write_attribute(metadata_key, value)
+ write_attribute(legacy_key, nil)
+ else
+ write_attribute(legacy_key, value)
+ metadata&.write_attribute(metadata_key, nil)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb
new file mode 100644
index 00000000000..1c78b1413a8
--- /dev/null
+++ b/app/models/concerns/ci/processable.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Ci
+ ##
+ # This module implements methods that need to be implemented by CI/CD
+ # entities that are supposed to go through pipeline processing
+ # services.
+ #
+ #
+ module Processable
+ def schedulable?
+ raise NotImplementedError
+ end
+
+ def action?
+ raise NotImplementedError
+ end
+
+ def when
+ read_attribute(:when) || 'on_success'
+ end
+
+ def expanded_environment_name
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 2c08a8e1acf..cf057d774cf 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ContainerRepository < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
+
belongs_to :project
validates :name, length: { minimum: 0, allow_nil: false }
@@ -8,6 +10,8 @@ class ContainerRepository < ActiveRecord::Base
delegate :client, to: :registry
+ scope :ordered, -> { order(:name) }
+
# rubocop: disable CodeReuse/ServiceClass
def registry
@registry ||= begin
@@ -39,11 +43,12 @@ class ContainerRepository < ActiveRecord::Base
end
def tags
- return @tags if defined?(@tags)
return [] unless manifest && manifest['tags']
- @tags = manifest['tags'].map do |tag|
- ContainerRegistry::Tag.new(self, tag)
+ strong_memoize(:tags) do
+ manifest['tags'].sort.map do |tag|
+ ContainerRegistry::Tag.new(self, tag)
+ end
end
end
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index 9bcc95e35a5..74aa04ab7d0 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone
@group_name = milestone.group.full_name
end
- def self.build_collection(groups)
- Milestone.of_groups(groups.select(:id))
+ def self.build_collection(groups, params)
+ milestones = Milestone.of_groups(groups.select(:id))
.reorder_by_due_date_asc
.order_by_name_asc
.active
- .map { |m| new(m) }
+ milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
+ milestones.map { |m| new(m) }
end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 4e82f3fed27..fd17745b035 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -27,6 +27,7 @@ class GlobalMilestone
items = Milestone.of_projects(projects)
.reorder_by_due_date_asc
.order_by_name_asc
+ items = items.search_title(params[:search_title]) if params[:search_title].present?
Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index a58537de319..97cb26c6ea9 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone
def self.build_collection(group, projects, params)
params =
- { state: params[:state] }
+ { state: params[:state], search_title: params[:search_title] }
project_milestones = Milestone.of_projects(projects)
+ project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present?
child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
grouped_milestones = child_milestones.group_by(&:title)
diff --git a/app/models/identity.rb b/app/models/identity.rb
index d63dd432426..acdde4f296b 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,7 +8,7 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: UniquenessScopes.scopes, case_sensitive: false }
- validates :user_id, uniqueness: { scope: UniquenessScopes.scopes }
+ validates :user, uniqueness: { scope: UniquenessScopes.scopes }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index e7168d49db9..e75c6eb2331 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -66,6 +66,17 @@ class InternalId < ActiveRecord::Base
InternalIdGenerator.new(subject, scope, usage, init).generate
end
+ # Flushing records is generally safe in a sense that those
+ # records are going to be re-created when needed.
+ #
+ # A filter condition has to be provided to not accidentally flush
+ # records for all projects.
+ def flush_records!(filter)
+ raise ArgumentError, "filter cannot be empty" if filter.blank?
+
+ where(filter).delete_all
+ end
+
def available?
@available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
end
@@ -111,7 +122,7 @@ class InternalId < ActiveRecord::Base
# Generates next internal id and returns it
def generate
- InternalId.transaction do
+ subject.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
#
@@ -125,7 +136,7 @@ class InternalId < ActiveRecord::Base
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
- InternalId.transaction do
+ subject.transaction do
(lookup || create_record).track_greatest_and_save!(new_value)
end
end
@@ -148,7 +159,7 @@ class InternalId < ActiveRecord::Base
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
def create_record
- InternalId.transaction(requires_new: true) do
+ subject.transaction(requires_new: true) do
InternalId.create!(
**scope,
usage: usage_value,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5c4ecbfdf4e..182c5d3d4b0 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -26,6 +26,8 @@ class Issue < ActiveRecord::Base
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
+ SORTING_PREFERENCE_FIELD = :issues_sort
+
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
diff --git a/app/models/lfs_download_object.rb b/app/models/lfs_download_object.rb
new file mode 100644
index 00000000000..6383f95d546
--- /dev/null
+++ b/app/models/lfs_download_object.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class LfsDownloadObject
+ include ActiveModel::Validations
+
+ attr_accessor :oid, :size, :link
+ delegate :sanitized_url, :credentials, to: :sanitized_uri
+
+ validates :oid, format: { with: /\A\h{64}\z/ }
+ validates :size, numericality: { greater_than_or_equal_to: 0 }
+ validates :link, public_url: { protocols: %w(http https) }
+
+ def initialize(oid:, size:, link:)
+ @oid = oid
+ @size = size
+ @link = link
+ end
+
+ def sanitized_uri
+ @sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index b0f049438eb..8e071a8ff21 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -78,12 +78,15 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
+ scope :with_user, -> (user) { where(user: user) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+ scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
+
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?, unless: :importing?
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index fc49ee7ac8c..2c9e1ba1d80 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,6 +12,8 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
+ scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
+
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 016c18ce6c8..5372c6084f4 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -12,6 +12,10 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
+ scope :in_namespaces, ->(groups) do
+ joins('INNER JOIN projects ON projects.id = members.source_id')
+ .where('projects.namespace_id in (?)', groups.select(:id))
+ end
class << self
# Add users to projects with passed access option
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7206d858dae..84cb8e1c50b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -21,6 +21,8 @@ class MergeRequest < ActiveRecord::Base
self.reactive_cache_refresh_interval = 10.minutes
self.reactive_cache_lifetime = 10.minutes
+ SORTING_PREFERENCE_FIELD = :merge_requests_sort
+
ignore_column :locked_at,
:ref_fetched,
:deleted_at
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8efaf87ca3a..26cfdc5ef30 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -77,7 +77,7 @@ class Milestone < ActiveRecord::Base
alias_attribute :name, :title
class << self
- # Searches for milestones matching the given query.
+ # Searches for milestones with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
@@ -88,6 +88,17 @@ class Milestone < ActiveRecord::Base
fuzzy_search(query, [:title, :description])
end
+ # Searches for milestones with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search_title(query)
+ fuzzy_search(query, [:title])
+ end
+
def filter_by_state(milestones, state)
case state
when 'closed' then milestones.closed
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a0bebc5e9a2..f7592532c5b 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Namespace < ActiveRecord::Base
+class Namespace < ApplicationRecord
include CacheMarkdownField
include Sortable
include Gitlab::VisibilityLevel
diff --git a/app/models/project.rb b/app/models/project.rb
index 15465d9b356..b385b89449d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
- access_level_attribute = ProjectFeature.access_level_attribute(feature)
- with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
+ access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
+ enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
+
+ with_project_feature.where(enabled_feature)
}
# Picks a feature where the level is exactly that given.
@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
- visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
+ min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin?
with_feature_enabled(feature)
@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base
column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature
- .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
- visible,
- ProjectFeature::PRIVATE,
- user.authorizations_for_projects)
+ .where(
+ "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
+ " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
+ {
+ private: Gitlab::VisibilityLevel::PRIVATE,
+ public_visible: ProjectFeature::ENABLED,
+ private_visible: ProjectFeature::PRIVATE,
+ authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
+ })
else
with_feature_access_level(feature, visible)
end
@@ -530,6 +538,7 @@ class Project < ActiveRecord::Base
def reference_pattern
%r{
+ (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
@@ -569,6 +578,14 @@ class Project < ActiveRecord::Base
end
end
+ def all_pipelines
+ if builds_enabled?
+ super
+ else
+ super.external
+ end
+ end
+
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
@@ -1585,6 +1602,13 @@ class Project < ActiveRecord::Base
def after_import
repository.after_import
wiki.repository.after_import
+
+ # The import assigns iid values on its own, e.g. by re-using GitHub ids.
+ # Flush existing InternalId records for this project for consistency reasons.
+ # Those records are going to be recreated with the next normal creation
+ # of a model instance (e.g. an Issue).
+ InternalId.flush_records!(project: self)
+
import_state.finish
import_state.remove_jid
update_project_counter_caches
@@ -1689,11 +1713,19 @@ class Project < ActiveRecord::Base
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
+ .concat(pages_variables)
.concat(container_registry_variables)
.concat(auto_devops_variables)
.concat(api_variables)
end
+ def pages_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host)
+ variables.append(key: 'CI_PAGES_URL', value: pages_url)
+ end
+ end
+
def api_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 39f2b8fe0de..f700090a493 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base
PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
class << self
def access_level_attribute(feature)
- feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
- raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+ feature = ensure_feature!(feature)
"#{feature}_access_level".to_sym
end
@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base
"#{table}.#{attribute}"
end
+
+ def required_minimum_access_level(feature)
+ feature = ensure_feature!(feature)
+
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
+ end
+
+ private
+
+ def ensure_feature!(feature)
+ feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+ raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+ feature
+ end
end
# Default scopes force us to unscope here since a service may need to check
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index a252052200a..71f5607dbdb 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -80,19 +80,27 @@ class BambooService < CiService
private
- def get_build_result_index
- # When Bamboo returns multiple results for a given changeset, arbitrarily assume the most relevant result to be the last one.
- -1
+ def get_build_result(response)
+ return if response.code != 200
+
+ # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
+ result = response.dig('results', 'results', 'result')
+
+ # In case of multiple results, arbitrarily assume the last one is the most relevant.
+ return result.last if result.is_a?(Array)
+
+ result
end
def read_build_page(response)
+ result = get_build_result(response)
key =
- if response.code != 200 || response.dig('results', 'results', 'size') == '0'
+ if result.blank?
# If actual build link can't be determined, send user to build summary page.
build_key
else
# If actual build link is available, go to build result page.
- response.dig('results', 'results', 'result', get_build_result_index, 'planResultKey', 'key')
+ result.dig('planResultKey', 'key')
end
build_url("browse/#{key}")
@@ -101,11 +109,15 @@ class BambooService < CiService
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
- status = if response.code == 404 || response.dig('results', 'results', 'size') == '0'
- 'Pending'
- else
- response.dig('results', 'results', 'result', get_build_result_index, 'buildState')
- end
+ result = get_build_result(response)
+ status =
+ if result.blank?
+ 'Pending'
+ else
+ result.dig('buildState')
+ end
+
+ return :error unless status.present?
if status.include?('Success')
'success'
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 33bc6a561f9..aeba2843e5d 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -74,6 +74,14 @@ class ProjectTeam
end
alias_method :users, :members
+ # `members` method uses project_authorizations table which
+ # is updated asynchronously, on project move it still contains
+ # old members who may not have access to the new location,
+ # so we filter out only members of project or project's group
+ def members_in_project_and_ancestors
+ members.where(id: member_user_ids)
+ end
+
def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
@@ -191,4 +199,8 @@ class ProjectTeam
def group
project.group
end
+
+ def member_user_ids
+ Member.on_project_and_ancestors(project).select(:user_id)
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4ef5bdc2d12..691abe3175f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,7 +2,7 @@
require 'carrierwave/orm/activerecord'
-class User < ActiveRecord::Base
+class User < ApplicationRecord
extend Gitlab::ConfigHelper
include Gitlab::ConfigHelper
@@ -754,8 +754,12 @@ class User < ActiveRecord::Base
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
- def authorizations_for_projects
- project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ def authorizations_for_projects(min_access_level: nil)
+ authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+
+ return authorizations unless min_access_level.present?
+
+ authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end
# Returns the projects this user has reporter (or greater) access to, limited
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index e42d78f47c5..2c90b8a73cd 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -10,6 +10,15 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
+ condition(:external_pipeline, scope: :subject, score: 0) do
+ @subject.external?
+ end
+
+ # Disallow users without permissions from accessing internal pipelines
+ rule { ~can?(:read_build) & ~external_pipeline }.policy do
+ prevent :read_pipeline
+ end
+
rule { protected_ref }.prevent :update_pipeline
rule { can?(:public_access) & branch_allows_collaboration }.policy do
diff --git a/app/policies/container_repository_policy.rb b/app/policies/container_repository_policy.rb
new file mode 100644
index 00000000000..6781c845142
--- /dev/null
+++ b/app/policies/container_repository_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ContainerRepositoryPolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index a0706eaa46c..dd8c5d49cf4 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -18,6 +18,7 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
+ prevent :create_note
end
rule { locked }.policy do
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index f22843b6463..8d23e3abed3 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -18,6 +18,7 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
+ prevent :award_emoji
end
rule { is_author }.policy do
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 040b5a73415..2b5cca76c20 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -28,7 +28,10 @@ class PersonalSnippetPolicy < BasePolicy
rule { anonymous }.prevent :comment_personal_snippet
- rule { can?(:comment_personal_snippet) }.enable :award_emoji
+ rule { can?(:comment_personal_snippet) }.policy do
+ enable :create_note
+ enable :award_emoji
+ end
rule { full_private_access }.enable :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 12f9f29dcc1..cadbc5ae009 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -108,6 +108,10 @@ class ProjectPolicy < BasePolicy
condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+ condition(:internal_builds_disabled) do
+ !@subject.builds_enabled?
+ end
+
features = %w[
merge_requests
issues
@@ -196,7 +200,6 @@ class ProjectPolicy < BasePolicy
enable :read_build
enable :read_container_image
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_merge_request
@@ -235,6 +238,7 @@ class ProjectPolicy < BasePolicy
enable :update_build
enable :create_pipeline
enable :update_pipeline
+ enable :read_pipeline_schedule
enable :create_pipeline_schedule
enable :create_merge_request_from
enable :create_wiki
@@ -314,13 +318,12 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:project_snippet))
end
- rule { wiki_disabled & ~has_external_wiki }.policy do
+ rule { wiki_disabled }.policy do
prevent(*create_read_update_admin_destroy(:wiki))
prevent(:download_wiki_code)
end
rule { builds_disabled | repository_disabled }.policy do
- prevent(*create_update_admin_destroy(:pipeline))
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
prevent(*create_read_update_admin_destroy(:environment))
@@ -328,11 +331,22 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:deployment))
end
+ # There's two separate cases when builds_disabled is true:
+ # 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
+ # - We do not prevent the user from accessing Pipelines to allow him to access external CI
+ # 2. When the user is not allowed to access CI - builds_disabled && ~internal_builds_disabled
+ # - We prevent the user from accessing Pipelines
+ rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
+ prevent(*create_read_update_admin_destroy(:pipeline))
+ prevent(*create_read_update_admin_destroy(:commit_status))
+ end
+
rule { repository_disabled }.policy do
prevent :push_code
prevent :download_code
prevent :fork_project
prevent :read_commit_status
+ prevent :read_pipeline
prevent(*create_read_update_admin_destroy(:release))
end
@@ -359,7 +373,6 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
- enable :read_pipeline_schedule
enable :read_commit_status
enable :read_container_image
enable :download_code
@@ -378,7 +391,6 @@ class ProjectPolicy < BasePolicy
rule { public_builds & can?(:guest_access) }.policy do
enable :read_pipeline
- enable :read_pipeline_schedule
end
# These rules are included to allow maintainers of projects to push to certain
@@ -393,7 +405,7 @@ class ProjectPolicy < BasePolicy
end.enable :read_issue_iid
rule do
- (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
+ (~guest & can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
end.enable :read_merge_request_iid
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 7dafa33bb99..e5e005cee6d 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -43,4 +43,6 @@ class ProjectSnippetPolicy < BasePolicy
enable :update_project_snippet
enable :admin_project_snippet
end
+
+ rule { ~can?(:read_project_snippet) }.prevent :create_note
end
diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb
new file mode 100644
index 00000000000..605c8f328a4
--- /dev/null
+++ b/app/presenters/ci/trigger_presenter.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class TriggerPresenter < Gitlab::View::Presenter::Delegated
+ presents :trigger
+
+ def has_token_exposed?
+ can?(current_user, :admin_trigger, trigger)
+ end
+
+ def token
+ if has_token_exposed?
+ trigger.token
+ else
+ trigger.short_token
+ end
+ end
+ end
+end
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
new file mode 100644
index 00000000000..05adbe1d4f5
--- /dev/null
+++ b/app/presenters/commit_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CommitPresenter < Gitlab::View::Presenter::Simple
+ presents :commit
+
+ def status_for(ref)
+ can?(current_user, :read_commit_status, commit.project) && commit.status(ref)
+ end
+
+ def any_pipelines?
+ can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any?
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 44b6ca299ae..c59e73f824c 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -170,6 +170,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
+ def can_read_pipeline?
+ pipeline && can?(current_user, :read_pipeline, pipeline)
+ end
+
def mergeable_discussions_state
# This avoids calling MergeRequest#mergeable_discussions_state without
# considering the state of the MR first. If a MR isn't mergeable, we can
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 7b1a0be75ca..62b23a889c8 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -4,6 +4,7 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
+ expose :version
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
index 59bf35f5aff..cc746698a05 100644
--- a/app/serializers/container_repository_entity.rb
+++ b/app/serializers/container_repository_entity.rb
@@ -3,7 +3,7 @@
class ContainerRepositoryEntity < Grape::Entity
include RequestAwareEntity
- expose :id, :path, :location
+ expose :id, :name, :path, :location, :created_at
expose :tags_path do |repository|
project_registry_repository_tags_path(project, repository, format: :json)
diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb
index 637294877f8..361c073e22e 100644
--- a/app/serializers/container_tag_entity.rb
+++ b/app/serializers/container_tag_entity.rb
@@ -3,7 +3,7 @@
class ContainerTagEntity < Grape::Entity
include RequestAwareEntity
- expose :name, :location, :revision, :short_revision, :total_size, :created_at
+ expose :name, :path, :location, :digest, :revision, :short_revision, :total_size, :created_at
expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
project_registry_repository_tag_path(project, tag.repository, tag.name)
diff --git a/app/serializers/error_tracking/project_entity.rb b/app/serializers/error_tracking/project_entity.rb
new file mode 100644
index 00000000000..405d87ca0d0
--- /dev/null
+++ b/app/serializers/error_tracking/project_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectEntity < Grape::Entity
+ expose(*Gitlab::ErrorTracking::Project::ACCESSORS)
+ end
+end
diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb
new file mode 100644
index 00000000000..b2406f4d631
--- /dev/null
+++ b/app/serializers/error_tracking/project_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectSerializer < BaseSerializer
+ entity ProjectEntity
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 9361c9f987b..f42abf06e1e 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -57,7 +57,7 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :merge_commit_message
- expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
# Booleans
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 446188347df..4a7ce00b8e2 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -10,7 +10,7 @@ module Ci
update_retried
new_builds =
- stage_indexes_of_created_builds.map do |index|
+ stage_indexes_of_created_processables.map do |index|
process_stage(index)
end
@@ -27,7 +27,7 @@ module Ci
return if HasStatus::BLOCKED_STATUS.include?(current_status)
if HasStatus::COMPLETED_STATUSES.include?(current_status)
- created_builds_in_stage(index).select do |build|
+ created_processables_in_stage(index).select do |build|
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
Ci::ProcessBuildService.new(project, @user)
.execute(build, current_status)
@@ -43,19 +43,19 @@ module Ci
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def stage_indexes_of_created_builds
- created_builds.order(:stage_idx).pluck('distinct stage_idx')
+ def stage_indexes_of_created_processables
+ created_processables.order(:stage_idx).pluck('distinct stage_idx')
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def created_builds_in_stage(index)
- created_builds.where(stage_idx: index)
+ def created_processables_in_stage(index)
+ created_processables.where(stage_idx: index)
end
# rubocop: enable CodeReuse/ActiveRecord
- def created_builds
- pipeline.builds.created
+ def created_processables
+ pipeline.processables.created
end
# This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
index f102e00d150..28879d2d67f 100644
--- a/app/services/concerns/exclusive_lease_guard.rb
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -6,9 +6,14 @@
#
# `#try_obtain_lease` takes a block which will be run if it was able to
# obtain the lease. Implement `#lease_timeout` to configure the timeout
-# for the exclusive lease. Optionally override `#lease_key` to set the
+# for the exclusive lease.
+#
+# Optionally override `#lease_key` to set the
# lease key, it defaults to the class name with underscores.
#
+# Optionally override `#lease_release?` to prevent the job to
+# be re-executed more often than LEASE_TIMEOUT.
+#
module ExclusiveLeaseGuard
extend ActiveSupport::Concern
@@ -23,7 +28,7 @@ module ExclusiveLeaseGuard
begin
yield lease
ensure
- release_lease(lease)
+ release_lease(lease) if lease_release?
end
end
@@ -40,6 +45,10 @@ module ExclusiveLeaseGuard
"#{self.class.name} does not implement #{__method__}"
end
+ def lease_release?
+ true
+ end
+
def release_lease(uuid)
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index ae0c644e6c0..f9717a9426b 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,9 +2,11 @@
module Members
class DestroyService < Members::BaseService
- def execute(member, skip_authorization: false)
+ def execute(member, skip_authorization: false, skip_subresources: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
+ @skip_auth = skip_authorization
+
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy
@@ -15,6 +17,7 @@ module Members
notification_service.decline_access_request(member)
end
+ delete_subresources(member) unless skip_subresources
enqueue_delete_todos(member)
after_execute(member: member)
@@ -24,6 +27,29 @@ module Members
private
+ def delete_subresources(member)
+ return unless member.is_a?(GroupMember) && member.user && member.group
+
+ delete_project_members(member)
+ delete_subgroup_members(member) if Group.supports_nested_objects?
+ end
+
+ def delete_project_members(member)
+ groups = member.group.self_and_descendants
+
+ ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member|
+ self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
+ end
+ end
+
+ def delete_subgroup_members(member)
+ groups = member.group.descendants
+
+ GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
+ self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
+ end
+ end
+
def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member)
end
diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb
index 7b92fe6fe14..bae98ede561 100644
--- a/app/services/notes/build_service.rb
+++ b/app/services/notes/build_service.rb
@@ -9,7 +9,7 @@ module Notes
if in_reply_to_discussion_id.present?
discussion = find_discussion(in_reply_to_discussion_id)
- unless discussion
+ unless discussion && can?(current_user, :create_note, discussion.noteable)
note = Note.new
note.errors.add(:base, 'Discussion to reply to cannot be found')
return note
@@ -34,19 +34,8 @@ module Notes
if project
project.notes.find_discussion(discussion_id)
else
- discussion = Note.find_discussion(discussion_id)
- noteable = discussion.noteable
-
- return nil unless noteable_without_project?(noteable)
-
- discussion
+ Note.find_discussion(discussion_id)
end
end
-
- def noteable_without_project?(noteable)
- return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
-
- false
- end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e1cf327209b..1a65561dd70 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -373,7 +373,8 @@ class NotificationService
end
def project_was_moved(project, old_path_with_namespace)
- recipients = notifiable_users(project.team.members, :mention, project: project)
+ recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
+ recipients = notifiable_users(recipients, :mention, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index c3cd9d1ea4a..fafdecb3222 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -62,7 +62,7 @@ module Projects
def rename_or_migrate_repository!
success =
if migrate_to_hashed_storage?
- ::Projects::HashedStorageMigrationService
+ ::Projects::HashedStorage::MigrationService
.new(project, full_path_before)
.execute
else
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
new file mode 100644
index 00000000000..488290db824
--- /dev/null
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ class CleanupTagsService < BaseService
+ def execute(container_repository)
+ return error('feature disabled') unless can_use?
+ return error('access denied') unless can_admin?
+
+ tags = container_repository.tags
+ tags_by_digest = group_by_digest(tags)
+
+ tags = without_latest(tags)
+ tags = filter_by_name(tags)
+ tags = with_manifest(tags)
+ tags = order_by_date(tags)
+ tags = filter_keep_n(tags)
+ tags = filter_by_older_than(tags)
+
+ deleted_tags = delete_tags(tags, tags_by_digest)
+
+ success(deleted: deleted_tags.map(&:name))
+ end
+
+ private
+
+ def delete_tags(tags_to_delete, tags_by_digest)
+ deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags|
+ delete_tag_digest(digest, tags, tags_by_digest[digest])
+ end
+
+ deleted_digests.values.flatten
+ end
+
+ def delete_tag_digest(digest, tags, other_tags)
+ # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/21405
+ # we have to remove all tags due
+ # to Docker Distribution bug unable
+ # to delete single tag
+ return unless tags.count == other_tags.count
+
+ # delete all tags
+ tags.map(&:delete)
+ end
+
+ def group_by_digest(tags)
+ tags.group_by(&:digest)
+ end
+
+ def without_latest(tags)
+ tags.reject(&:latest?)
+ end
+
+ def with_manifest(tags)
+ tags.select(&:valid?)
+ end
+
+ def order_by_date(tags)
+ now = DateTime.now
+ tags.sort_by { |tag| tag.created_at || now }.reverse
+ end
+
+ def filter_by_name(tags)
+ regex = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex']}\\z")
+
+ tags.select do |tag|
+ regex.scan(tag.name).any?
+ end
+ end
+
+ def filter_keep_n(tags)
+ tags.drop(params['keep_n'].to_i)
+ end
+
+ def filter_by_older_than(tags)
+ return tags unless params['older_than']
+
+ older_than = ChronicDuration.parse(params['older_than']).seconds.ago
+
+ tags.select do |tag|
+ tag.created_at && tag.created_at < older_than
+ end
+ end
+
+ def can_admin?
+ can?(current_user, :admin_container_image, project)
+ end
+
+ def can_use?
+ Feature.enabled?(:container_registry_cleanup, project, default_enabled: true)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
new file mode 100644
index 00000000000..761c81d776f
--- /dev/null
+++ b/app/services/projects/hashed_storage/base_repository_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ # Returned when there is an error with the Hashed Storage migration
+ RepositoryMigrationError = Class.new(StandardError)
+
+ # Returned when there is an error with the Hashed Storage rollback
+ RepositoryRollbackError = Class.new(StandardError)
+
+ class BaseRepositoryService < BaseService
+ include Gitlab::ShellAdapter
+
+ attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki
+
+ def initialize(project, old_disk_path, logger: nil)
+ @project = project
+ @logger = logger || Gitlab::AppLogger
+ @old_disk_path = old_disk_path
+ @old_wiki_disk_path = "#{old_disk_path}.wiki"
+ @move_wiki = has_wiki?
+ end
+
+ protected
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def has_wiki?
+ gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def move_repository(from_name, to_name)
+ from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
+
+ # If we don't find the repository on either original or target we should log that as it could be an issue if the
+ # project was not originally empty.
+ if !from_exists && !to_exists
+ logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
+ return false
+ elsif !from_exists
+ # Repository have been moved already.
+ return true
+ end
+
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def rollback_folder_move
+ move_repository(new_disk_path, old_disk_path)
+ move_repository("#{new_disk_path}.wiki", old_wiki_disk_path)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index a1f0302aeb7..03e0685d2cd 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -12,6 +12,7 @@ module Projects
@logger = logger || Rails.logger
@old_disk_path = old_disk_path
@new_disk_path = project.disk_path
+ @skipped = false
end
def execute
@@ -32,24 +33,29 @@ module Projects
result
end
+ def skipped?
+ @skipped
+ end
+
private
- def move_folder!(old_disk_path, new_disk_path)
- unless File.directory?(old_disk_path)
- logger.info("Skipped attachments migration from '#{old_disk_path}' to '#{new_disk_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
- return
+ def move_folder!(old_path, new_path)
+ unless File.directory?(old_path)
+ logger.info("Skipped attachments migration from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
+ @skipped = true
+ return true
end
- if File.exist?(new_disk_path)
- logger.error("Cannot migrate attachments from '#{old_disk_path}' to '#{new_disk_path}', target path already exist (PROJECT_ID=#{project.id})")
- raise AttachmentMigrationError, "Target path '#{new_disk_path}' already exist"
+ if File.exist?(new_path)
+ logger.error("Cannot migrate attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
+ raise AttachmentMigrationError, "Target path '#{new_path}' already exist"
end
# Create hashed storage base path folder
- FileUtils.mkdir_p(File.dirname(new_disk_path))
+ FileUtils.mkdir_p(File.dirname(new_path))
- FileUtils.mv(old_disk_path, new_disk_path)
- logger.info("Migrated project attachments from '#{old_disk_path}' to '#{new_disk_path}' (PROJECT_ID=#{project.id})")
+ FileUtils.mv(old_path, new_path)
+ logger.info("Migrated project attachments from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")
true
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 2d851866a18..9c672283c7e 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -2,21 +2,7 @@
module Projects
module HashedStorage
- RepositoryMigrationError = Class.new(StandardError)
-
- class MigrateRepositoryService < BaseService
- include Gitlab::ShellAdapter
-
- attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki
-
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @logger = logger || Rails.logger
- @old_disk_path = old_disk_path
- @old_wiki_disk_path = "#{old_disk_path}.wiki"
- @move_wiki = has_wiki?
- end
-
+ class MigrateRepositoryService < BaseRepositoryService
def execute
try_to_set_repository_read_only!
@@ -61,36 +47,6 @@ module Projects
raise RepositoryMigrationError, migration_error
end
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def has_wiki?
- gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
-
- # If we don't find the repository on either original or target we should log that as it could be an issue if the
- # project was not originally empty.
- if !from_exists && !to_exists
- logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
- return false
- elsif !from_exists
- # Repository have been moved already.
- return true
- end
-
- gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def rollback_folder_move
- move_repository(new_disk_path, old_disk_path)
- move_repository("#{new_disk_path}.wiki", old_wiki_disk_path)
- end
end
end
end
diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb
new file mode 100644
index 00000000000..f132dca61c9
--- /dev/null
+++ b/app/services/projects/hashed_storage/migration_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Projects
+ module HashedStorage
+ class MigrationService < BaseService
+ attr_reader :logger, :old_disk_path
+
+ def initialize(project, old_disk_path, logger: nil)
+ @project = project
+ @old_disk_path = old_disk_path
+ @logger = logger || Gitlab::AppLogger
+ end
+
+ def execute
+ # Migrate repository from Legacy to Hashed Storage
+ unless project.hashed_storage?(:repository)
+ return false unless migrate_repository
+ end
+
+ # Migrate attachments from Legacy to Hashed Storage
+ unless project.hashed_storage?(:attachments)
+ return false unless migrate_attachments
+ end
+
+ true
+ end
+
+ private
+
+ def migrate_repository
+ HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute
+ end
+
+ def migrate_attachments
+ HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute
+ end
+ end
+ end
+end
diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb
deleted file mode 100644
index a0e734005f8..00000000000
--- a/app/services/projects/hashed_storage_migration_service.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- class HashedStorageMigrationService < BaseService
- attr_reader :logger, :old_disk_path
-
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @old_disk_path = old_disk_path
- @logger = logger || Rails.logger
- end
-
- def execute
- # Migrate repository from Legacy to Hashed Storage
- unless project.hashed_storage?(:repository)
- return unless HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute
- end
-
- # Migrate attachments from Legacy to Hashed Storage
- unless project.hashed_storage?(:attachments)
- HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute
- end
-
- true
- end
- end
-end
diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb
new file mode 100644
index 00000000000..a0fc5149bb4
--- /dev/null
+++ b/app/services/projects/import_error_filter.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Projects
+ # Used by project imports, it removes any potential paths
+ # included in an error message that could be stored in the DB
+ class ImportErrorFilter
+ ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
+ FILTER_MESSAGE = '[FILTERED]'
+
+ def self.filter_message(message)
+ message.gsub(ERROR_MESSAGE_FILTER, FILTER_MESSAGE)
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 0c426faa22d..5861b803996 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -24,8 +24,16 @@ module Projects
import_data
success
- rescue => e
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}")
+ rescue => e
+ message = Projects::ImportErrorFilter.filter_message(e.message)
+
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
+
+ error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}")
end
private
@@ -35,7 +43,7 @@ module Projects
begin
Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Error, "Blocked import URL: #{e.message}"
+ raise e, "Blocked import URL: #{e.message}"
end
end
@@ -86,11 +94,11 @@ module Projects
return unless project.lfs_enabled?
- oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- download_service = Projects::LfsPointers::LfsDownloadService.new(project)
+ lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
- oids_to_download.each do |oid, link|
- download_service.execute(oid, link)
+ lfs_objects_to_download.each do |lfs_download_object|
+ Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
+ .execute
end
rescue => e
# Right now, to avoid aborting the importing process, we silently fail
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index a837ea82e38..7998976b00a 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -41,16 +41,17 @@ module Projects
end
def parse_response_links(objects_response)
- objects_response.each_with_object({}) do |entry, link_list|
+ objects_response.each_with_object([]) do |entry, link_list|
begin
- oid = entry['oid']
link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
raise DownloadLinkNotFound unless link
- link_list[oid] = add_credentials(link)
- rescue DownloadLinkNotFound, URI::InvalidURIError
- Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
+ link_list << LfsDownloadObject.new(oid: entry['oid'],
+ size: entry['size'],
+ link: add_credentials(link))
+ rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
+ log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
end
end
end
@@ -70,7 +71,7 @@ module Projects
end
def add_credentials(link)
- uri = URI.parse(link)
+ uri = Addressable::URI.parse(link)
if should_add_credentials?(uri)
uri.user = remote_uri.user
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index b5128443435..398f00a598d 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,68 +4,93 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
- VALID_PROTOCOLS = %w[http https].freeze
+ SizeError = Class.new(StandardError)
+ OidError = Class.new(StandardError)
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(oid, url)
- return unless project&.lfs_enabled? && oid.present? && url.present?
+ attr_reader :lfs_download_object
+ delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
- return if LfsObject.exists?(oid: oid)
+ def initialize(project, lfs_download_object)
+ super(project)
- sanitized_uri = sanitize_url!(url)
+ @lfs_download_object = lfs_download_object
+ end
- with_tmp_file(oid) do |file|
- download_and_save_file(file, sanitized_uri)
- lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ return unless project&.lfs_enabled? && lfs_download_object
+ return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
+ return if LfsObject.exists?(oid: lfs_oid)
- project.all_lfs_objects << lfs_object
+ wrap_download_errors do
+ download_lfs_file!
end
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}")
- rescue StandardError => e
- Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
end
# rubocop: enable CodeReuse/ActiveRecord
private
- def sanitize_url!(url)
- Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri|
- # Just validate that HTTP/HTTPS protocols are used. The
- # subsequent Gitlab::HTTP.get call will do network checks
- # based on the settings.
- Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url,
- protocols: VALID_PROTOCOLS)
+ def wrap_download_errors(&block)
+ yield
+ rescue SizeError, OidError, StandardError => e
+ error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}")
+ end
+
+ def download_lfs_file!
+ with_tmp_file do |tmp_file|
+ download_and_save_file!(tmp_file)
+ project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
+ size: lfs_size,
+ file: tmp_file)
+
+ success
end
end
- def download_and_save_file(file, sanitized_uri)
- response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
+ def download_and_save_file!(file)
+ digester = Digest::SHA256.new
+ response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
+ digester << fragment
file.write(fragment)
+
+ raise_size_error! if file.size > lfs_size
end
raise StandardError, "Received error code #{response.code}" unless response.success?
- end
- def headers(sanitized_uri)
- query_options.tap do |headers|
- credentials = sanitized_uri.credentials
+ raise_size_error! if file.size != lfs_size
+ raise_oid_error! if digester.hexdigest != lfs_oid
+ end
- if credentials[:user].present? || credentials[:password].present?
+ def download_headers
+ { stream_body: true }.tap do |headers|
+ if lfs_credentials[:user].present? || lfs_credentials[:password].present?
# Using authentication headers in the request
- headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
+ headers[:basic_auth] = { username: lfs_credentials[:user], password: lfs_credentials[:password] }
end
end
end
- def query_options
- { stream_body: true }
- end
-
- def with_tmp_file(oid)
+ def with_tmp_file
create_tmp_storage_dir
- File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
+ File.open(tmp_filename, 'wb') do |file|
+ begin
+ yield file
+ rescue StandardError => e
+ # If the lfs file is successfully downloaded it will be removed
+ # when it is added to the project's lfs files.
+ # Nevertheless if any excetion raises the file would remain
+ # in the file system. Here we ensure to remove it
+ File.unlink(file) if File.exist?(file)
+
+ raise e
+ end
+ end
+ end
+
+ def tmp_filename
+ File.join(tmp_storage_dir, lfs_oid)
end
def create_tmp_storage_dir
@@ -79,6 +104,20 @@ module Projects
def storage_dir
@storage_dir ||= Gitlab.config.lfs.storage_path
end
+
+ def raise_size_error!
+ raise SizeError, 'Size mistmatch'
+ end
+
+ def raise_oid_error!
+ raise OidError, 'Oid mismatch'
+ end
+
+ def error(message, http_status = nil)
+ log_error(message)
+
+ super
+ end
end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index eb2478be3cf..5caeb4cfa5f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -7,7 +7,11 @@ module Projects
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
- SITE_PATH = 'public/'.freeze
+ PUBLIC_DIR = 'public'.freeze
+
+ # this has to be invalid group name,
+ # as it shares the namespace with groups
+ TMP_EXTRACT_PATH = '@pages.tmp'.freeze
attr_reader :build
@@ -27,12 +31,11 @@ module Projects
raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
- FileUtils.mkdir_p(tmp_path)
- Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ make_secure_tmp_dir(tmp_path) do |archive_path|
extract_archive!(archive_path)
# Check if we did extract public directory
- archive_public_path = File.join(archive_path, 'public')
+ archive_public_path = File.join(archive_path, PUBLIC_DIR)
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvalidStateError, 'pages are outdated' unless latest?
@@ -85,22 +88,18 @@ module Projects
raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
- public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+ public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true)
if public_entry.total_size > max_size
raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
- # Requires UnZip at least 6.00 Info-ZIP.
- # -qq be (very) quiet
- # -n never overwrite existing files
- # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
- site_path = File.join(SITE_PATH, '*')
build.artifacts_file.use_file do |artifacts_path|
- unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
- raise FailedToExtractError, 'pages failed to extract'
- end
+ SafeZip::Extract.new(artifacts_path)
+ .extract(directories: [PUBLIC_DIR], to: temp_path)
end
+ rescue SafeZip::Extract::Error => e
+ raise FailedToExtractError, e.message
end
def deploy_page!(archive_public_path)
@@ -139,7 +138,7 @@ module Projects
end
def tmp_path
- @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ @tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH)
end
def pages_path
@@ -147,11 +146,11 @@ module Projects
end
def public_path
- @public_path ||= File.join(pages_path, 'public')
+ @public_path ||= File.join(pages_path, PUBLIC_DIR)
end
def previous_public_path
- @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ @previous_public_path ||= File.join(pages_path, "#{PUBLIC_DIR}.#{SecureRandom.hex}")
end
def ref
@@ -188,5 +187,15 @@ module Projects
def pages_deployments_failed_total_counter
@pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
end
+
+ def make_secure_tmp_dir(tmp_path)
+ FileUtils.mkdir_p(tmp_path)
+ path = Dir.mktmpdir(nil, tmp_path)
+ begin
+ yield(path)
+ ensure
+ FileUtils.remove_entry_secure(path)
+ end
+ end
end
end
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index 4340d3e8260..9b85e13107b 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -6,8 +6,6 @@ module ProtectedBranches
@push_params = AccessLevelParams.new(:push, params)
@merge_params = AccessLevelParams.new(:merge, params)
- verify_params!
-
protected_branch_params = {
name: params[:name],
push_access_levels_attributes: @push_params.access_levels,
@@ -16,11 +14,5 @@ module ProtectedBranches
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end
-
- private
-
- def verify_params!
- # EE-only
- end
end
end
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
index cc47b46b527..1f720fc835f 100644
--- a/app/services/suggestions/apply_service.rb
+++ b/app/services/suggestions/apply_service.rb
@@ -11,7 +11,7 @@ module Suggestions
return error('Suggestion is not appliable')
end
- unless latest_diff_refs?(suggestion)
+ unless latest_source_head?(suggestion)
return error('The file has been changed')
end
@@ -29,12 +29,13 @@ module Suggestions
private
- # Checks whether the latest diff refs for the branch matches with
- # the position refs we're using to update the file content. Since
- # the persisted refs are updated async (for MergeRequest),
- # it's more consistent to fetch this data directly from the repository.
- def latest_diff_refs?(suggestion)
- suggestion.position.diff_refs == suggestion.noteable.repository_diff_refs
+ # Checks whether the latest source branch HEAD matches with
+ # the position HEAD we're using to update the file content. Since
+ # the persisted HEAD is updated async (for MergeRequest),
+ # it's more consistent to fetch this data directly from the
+ # repository.
+ def latest_source_head?(suggestion)
+ suggestion.position.head_sha == suggestion.noteable.source_branch_sha
end
def file_update_params(suggestion)
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index c6c29ed1f21..7a2bbfcdc4d 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -11,7 +11,6 @@
.form-text.text-muted
Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
- %em (EXPERIMENTAL)
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'label-bold'
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index b75454b33d7..ec57eb1ed08 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -18,12 +18,12 @@
.table-mobile-content
= link_to runner.short_sha, admin_runner_path(runner)
- .table-section.section-15
+ .table-section.section-20
.table-mobile-header{ role: 'rowheader' }= _('Description')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.description }
= runner.description
- .table-section.section-15
+ .table-section.section-10
.table-mobile-header{ role: 'rowheader' }= _('Version')
.table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
= runner.version
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index e9e4e0847d3..81380587fd2 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -106,8 +106,8 @@
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'rowheader' }= _('Type')
.table-section.section-10{ role: 'rowheader' }= _('Runner token')
- .table-section.section-15{ role: 'rowheader' }= _('Description')
- .table-section.section-15{ role: 'rowheader' }= _('Version')
+ .table-section.section-20{ role: 'rowheader' }= _('Description')
+ .table-section.section-10{ role: 'rowheader' }= _('Version')
.table-section.section-10{ role: 'rowheader' }= _('IP Address')
.table-section.section-5{ role: 'rowheader' }= _('Projects')
.table-section.section-5{ role: 'rowheader' }= _('Jobs')
diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_form.html.haml
index 4c47e11927e..4c47e11927e 100644
--- a/app/views/clusters/clusters/_integration_form.html.haml
+++ b/app/views/clusters/clusters/_form.html.haml
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 85d1002243b..a9299af8d78 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -8,5 +8,5 @@
%h4= s_('ClusterIntegration|Did you know?')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-default{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
- Apply for credit
+ = s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml
deleted file mode 100644
index e9f05eaf453..00000000000
--- a/app/views/clusters/clusters/gcp/_show.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-.form-group
- %label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Kubernetes cluster name')
- .input-group
- %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
- %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: clusterable.cluster_path(@cluster), as: :cluster do |field|
- = form_errors(@cluster)
-
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
- .form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
- .input-group
- = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
- %span.input-group-append
- = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
-
- .form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
- .input-group
- = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
- %span.input-group-append.clipboard-addon
- = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
-
- .form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
- .input-group
- = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
- %span.input-group-append
- %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
- = s_('ClusterIntegration|Show')
- = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
-
- - if @cluster.allow_user_defined_namespace?
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
-
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
-
- .form-group
- = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 58d0a304363..9bab3bf56aa 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title 'Kubernetes'
-- page_title "Kubernetes Clusters"
+- breadcrumb_title _('Kubernetes')
+- page_title _('Kubernetes Clusters')
= render_gcp_signup_offer
@@ -9,12 +9,12 @@
- else
.top-area.adjust
.nav-text
- = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
+ = s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project')
= render 'clusters/clusters/buttons'
- if @has_ancestor_clusters
.bs-callout.bs-callout-info
- = s_("ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.")
+ = s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong
= link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index eeeef6bd824..6a8af23e5e8 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title 'Kubernetes'
-- page_title _("Kubernetes Cluster")
+- breadcrumb_title _('Kubernetes')
+- page_title _('Kubernetes Cluster')
- active_tab = local_assigns.fetch(:active_tab, 'gcp')
= javascript_include_tag 'https://apis.google.com/js/api.js'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 89a2dfdd69f..1ef76ef801e 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path
+- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title @cluster.name
-- page_title _("Kubernetes Cluster")
+- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- expanded = Rails.env.test?
@@ -31,7 +31,7 @@
%section#cluster-integration
%h4= @cluster.name
= render 'banner'
- = render 'integration_form'
+ = render 'form'
.cluster-applications-table#js-cluster-applications
@@ -39,19 +39,16 @@
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
- - if @cluster.managed?
- = render 'clusters/clusters/gcp/show'
- - else
- = render 'clusters/clusters/user/show'
+ = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster)
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
= render 'advanced_settings'
diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml
deleted file mode 100644
index cac8e72edd3..00000000000
--- a/app/views/clusters/clusters/user/_show.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-= 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'
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
-
- = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
- .form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold'
- = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
-
- .form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
-
- .form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
- .input-group
- = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
- %span.input-group-append.clipboard-addon
- .input-group-text
- %button.js-show-cluster-token.btn-blank{ type: 'button' }
- = s_('ClusterIntegration|Show')
-
- - if @cluster.allow_user_defined_namespace?
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
-
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
-
- .form-group
- = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml
new file mode 100644
index 00000000000..4a452b83112
--- /dev/null
+++ b/app/views/clusters/platforms/kubernetes/_form.html.haml
@@ -0,0 +1,58 @@
+= form_for cluster, url: update_cluster_url_path, as: :cluster do |field|
+ = form_errors(cluster)
+
+ .form-group
+ - if cluster.managed?
+ %label.append-bottom-10{ for: 'cluster-name' }
+ = s_('ClusterIntegration|Kubernetes cluster name')
+ .input-group
+ %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true }
+ %span.input-group-append
+ = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
+ - else
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
+ .input-group
+ = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
+
+ = field.fields_for :platform_kubernetes, platform do |platform_field|
+ .form-group
+ = platform_field.label :api_url, s_('ClusterIntegration|API URL')
+ .input-group
+ = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.managed?
+ - if cluster.managed?
+ %span.input-group-append
+ = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
+
+ .form-group
+ = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ .input-group
+ = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.managed?
+ - if cluster.managed?
+ %span.input-group-append.clipboard-addon
+ = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
+
+ .form-group
+ = platform_field.label :token, s_('ClusterIntegration|Token')
+ .input-group
+ = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.managed?
+ %span.input-group-append
+ %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
+ = s_('ClusterIntegration|Show')
+ - if cluster.managed?
+ = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
+
+ - if cluster.allow_user_defined_namespace?
+ .form-group
+ = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+
+ .form-group
+ .form-check
+ = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
+ = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index ae0e38bf0ee..13822d36f15 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -13,6 +13,8 @@
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
+ .nav-controls
+ = render 'shared/milestones/search_form'
.milestones
%ul.content-list
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
index 3d0a1f622a5..ccc3e734276 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -1,5 +1,5 @@
#content
- = email_default_heading("#{@resource.user.name}, you've added an additional email!")
+ = email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!")
%p Click the link below to confirm your email address (#{@resource.email})
#cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 88e401081f4..3a8d95f44d1 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,17 +1,58 @@
-.group-home-panel.text-center.border-bottom
- %div{ class: container_class }
- .avatar-container.s70.group-avatar
- = group_icon(@group, class: "avatar s70 avatar-tile")
- %h1.group-title
- = @group.name
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+- can_create_subgroups = can?(current_user, :create_subgroup, @group)
- - if @group.description.present?
- .group-home-desc
- = markdown_field(@group, :description)
+.group-home-panel
+ .row.mb-3
+ .home-panel-title-row.col-md-12.col-lg-6.d-flex
+ .avatar-container.home-panel-avatar.append-right-default.float-none
+ = group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64)
+ .d-flex.flex-column.flex-wrap.align-items-baseline
+ .d-inline-flex.align-items-baseline
+ %h1.home-panel-title.prepend-top-8.append-bottom-5
+ = @group.name
+ %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false, options: {class: 'icon'})
+ .home-panel-metadata.d-flex.align-items-center.text-secondary
+ %span
+ = _("Group")
+ - if current_user
+ %span.access-request-links.prepend-left-8
+ = render 'shared/members/access_request_links', source: @group
- - if current_user
- .group-buttons.d-none.d-sm-block
- = render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
+ - if current_user
+ .group-buttons
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn'
+ - if can? current_user, :create_projects, @group
+ - new_project_label = _("New project")
+ - new_subgroup_label = _("New subgroup")
+ - if can_create_subgroups
+ .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.prepend-top-default.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
+ %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
+ = sprite_icon("arrow-down", css_class: "icon dropdown-btn-icon")
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
+ %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_project_label
+ %span= s_("GroupsTree|Create a project in this group.")
+ %li.divider.droplap-item-ignore
+ %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_subgroup_label
+ %span= s_("GroupsTree|Create a subgroup in this group.")
+ - else
+ = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
+
+ - if @group.description.present?
+ .group-home-desc.mt-1
+ .home-panel-description
+ .home-panel-description-markdown.read-more-container
+ = markdown_field(@group, :description)
+ %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
+ = _("Read more")
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index b3d13a2dc43..b0ba846f204 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,20 +1,20 @@
= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+ = form_errors(@milestone)
.row
- = form_errors(@milestone)
-
.col-md-6
.form-group.row
- = f.label :title, "Title", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :title, "Title"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, "Description", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :description, "Description"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
- .clearfix
- .error-alert
-
+ .clearfix
+ .error-alert
= render "shared/milestones/form_dates", f: f
.form-actions
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index af4fe8f2ef8..b6fb908c8f6 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -4,6 +4,7 @@
= render 'shared/milestones_filter', counts: @milestone_states
.nav-controls
+ = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success"
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index cc294f6a931..77fe88dacb7 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,66 +1,41 @@
- @no_container = true
- breadcrumb_title _("Details")
-- can_create_subgroups = can?(current_user, :create_subgroup, @group)
+- @content_class = "limit-container-width" unless fluid_layout
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
-= render 'groups/home_panel'
-
-.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
- .top-area.group-nav-container
- .group-search
- = render "shared/groups/search_form"
- - if can? current_user, :create_projects, @group
- - new_project_label = _("New project")
- - new_subgroup_label = _("New subgroup")
- - if can_create_subgroups
- .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
- %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
- = icon("caret-down", class: "dropdown-btn-icon")
- %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
- %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
- .menu-item
- .icon-container
- = icon("check", class: "list-item-checkmark")
- .description
- %strong= new_project_label
- %span= s_("GroupsTree|Create a project in this group.")
- %li.divider.droplap-item-ignore
- %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
- .menu-item
- .icon-container
- = icon("check", class: "list-item-checkmark")
- .description
- %strong= new_subgroup_label
- %span= s_("GroupsTree|Create a subgroup in this group.")
- - else
- = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success"
-
- .scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
- %li.js-subgroups_and_projects-tab
- = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
- = _("Subgroups and projects")
- %li.js-shared-tab
- = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
- = _("Shared projects")
- %li.js-archived-tab
- = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
- = _("Archived projects")
-
- .nav-controls
- = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
-
- .tab-content
- #subgroups_and_projects.tab-pane
- = render "subgroups_and_projects", group: @group
-
- #shared.tab-pane
- = render "shared_projects", group: @group
-
- #archived.tab-pane
- = render "archived_projects", group: @group
+%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ = render 'groups/home_panel'
+
+ .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
+ .top-area.group-nav-container
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
+ %li.js-subgroups_and_projects-tab
+ = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do
+ = _("Subgroups and projects")
+ %li.js-shared-tab
+ = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do
+ = _("Shared projects")
+ %li.js-archived-tab
+ = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do
+ = _("Archived projects")
+
+ .nav-controls.d-block.d-md-flex
+ .group-search
+ = render "shared/groups/search_form"
+
+ = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
+
+ .tab-content
+ #subgroups_and_projects.tab-pane
+ = render "subgroups_and_projects", group: @group
+
+ #shared.tab-pane
+ = render "shared_projects", group: @group
+
+ #archived.tab-pane
+ = render "archived_projects", group: @group
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index b24d6e27536..057225d021f 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -4,7 +4,7 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
-#ide.ide-loading{ data: ide_data() }
+#ide.ide-loading{ data: ide_data }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index ef69197e453..9280f12e187 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -56,7 +56,7 @@
.project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :extra_group
- - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.project_key, path: repo.project_key) } : {}
+ - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {}
= select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 513902890af..cd9128c452b 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -1,12 +1,8 @@
-- show_blog_link = current_user_menu?(:help) && blog_post_url.present?
%ul
- if current_user_menu?(:help)
%li
= link_to _("Help"), help_path
%li.divider
- - if show_blog_link
- %li
- = link_to _("What's new?"), blog_post_url
%li
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 6442e61da0d..dd7833647b7 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -26,7 +26,7 @@
%span= _('Details')
= nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
+ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity qa-activity-link' do
%span= _('Activity')
- if project_nav_tab?(:releases)
@@ -146,7 +146,7 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests qa-merge-requests-link' do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
@@ -281,19 +281,34 @@
%strong.fly-out-top-item-name
= _('Registry')
- - if project_nav_tab? :wiki
+ - if project_nav_tab?(:wiki)
+ - wiki_url = project_wiki_path(@project, :home)
= nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
+ = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
= _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
- = link_to get_project_wiki_path(@project) do
+ = link_to wiki_url do
%strong.fly-out-top-item-name
= _('Wiki')
+ - if project_nav_tab?(:external_wiki)
+ - external_wiki_url = @project.external_wiki.external_wiki_url
+ = nav_link do
+ = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
+ .nav-icon-container
+ = sprite_icon('issue-external')
+ %span.nav-item-name
+ = _('External Wiki')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(html_options: { class: "fly-out-top-item" } ) do
+ = link_to external_wiki_url do
+ %strong.fly-out-top-item-name
+ = _('External Wiki')
+
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index 50209c46ed1..5a67214059c 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -3,7 +3,7 @@
<% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%>
-<%= note.author_name -%>
+<%= sanitize_name(note.author_name) -%>
<% if discussion.new_discussion? -%>
<%= " started a new discussion" -%>
<% else -%>
@@ -16,7 +16,7 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%>
-<%= "#{note.author_name} commented:" -%>
+<%= "#{sanitize_name(note.author_name)} commented:" -%>
<% end -%>
diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb
index 695780c3145..bf863952478 100644
--- a/app/views/notify/autodevops_disabled_email.text.erb
+++ b/app/views/notify/autodevops_disabled_email.text.erb
@@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%>
- Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index b7284dd819b..eb148d72da1 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{@updated_by.name}
+ Issue was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index b35d4b7502d..b1f0a3f37ec 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{@updated_by.name}
+Issue was closed by #{sanitize_name(@updated_by.name)}
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 44e018304e1..2aa753e0d55 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index c4e06cb3cb1..1094d584a1c 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml
index b6051b11cea..66e73a9b03f 100644
--- a/app/views/notify/issue_status_changed_email.html.haml
+++ b/app/views/notify/issue_status_changed_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was #{@issue_status} by #{@updated_by.name}
+ Issue was #{@issue_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb
index 4200881f7e8..f38b09e9820 100644
--- a/app/views/notify/issue_status_changed_email.text.erb
+++ b/app/views/notify/issue_status_changed_email.text.erb
@@ -1,4 +1,4 @@
-Issue was <%= @issue_status %> by <%= @updated_by.name %>
+Issue was <%= @issue_status %> by <%= sanitize_name(@updated_by.name) %>
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
index 9c5ee0eaf26..ddb4a7b3d2c 100644
--- a/app/views/notify/member_access_requested_email.text.erb
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= sanitize_name(member.user.name) %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
index cef87101427..c824533eac2 100644
--- a/app/views/notify/member_invite_accepted_email.text.erb
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -1,3 +1,3 @@
-<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<%= member.invite_email %>, now known as <%= sanitize_name(member.user.name) %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
index 0a6393355be..d944c3b4a50 100644
--- a/app/views/notify/member_invited_email.text.erb
+++ b/app/views/notify/member_invited_email.text.erb
@@ -1,4 +1,4 @@
-You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index b487e26b122..ffb416abf72 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+ Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index ae2a2933865..b9b9e0c3ad7 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,8 +1,8 @@
-Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
+Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index dcdd6db69d6..0c7bf1bb044 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 661c23bcbe2..045a43cbc84 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
-Author: #{@merge_request.author_name}
-Assignee: #{@merge_request.assignee_name}
+Author: #{sanitize_name(@merge_request.author_name)}
+Assignee: #{sanitize_name(@merge_request.assignee_name)}
diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml
index 4b9350c4e88..b857705e01f 100644
--- a/app/views/notify/new_gpg_key_email.html.haml
+++ b/app/views/notify/new_gpg_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new GPG key was added to your account:
%p
diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb
index 80b5a1fd7ff..92ea851eee4 100644
--- a/app/views/notify/new_gpg_key_email.text.erb
+++ b/app/views/notify/new_gpg_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new GPG key was added to your account:
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index 3c716f77296..58a2bcbe5eb 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -1,7 +1,7 @@
New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
+Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 23213106c5b..173091e4a80 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -1,7 +1,7 @@
You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
-Author: <%= @issue.author_name %>
-Assignee: <%= @issue.assignee_list %>
+Author: <%= sanitize_name(@issue.author_name) %>
+Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 6fcebb22fc4..96a4f3f9eac 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= merge_path_description(@merge_request, 'to') %>
-Author: <%= @merge_request.author_name %>
-Assignee: <%= @merge_request.assignee_name %>
+Author: <%= sanitize_name(@merge_request.author_name) %>
+Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 5acd45b74a7..db23447dd39 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -7,7 +7,7 @@
- if @merge_request.assignee_id.present?
%p
- Assignee: #{@merge_request.assignee_name}
+ Assignee: #{sanitize_name(@merge_request.assignee_name)}
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml
index 63b0cbbd205..d031842be95 100644
--- a/app/views/notify/new_ssh_key_email.html.haml
+++ b/app/views/notify/new_ssh_key_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user.name}!
+ Hi #{sanitize_name(@user.name)}!
%p
A new public key was added to your account:
%p
diff --git a/app/views/notify/new_ssh_key_email.text.erb b/app/views/notify/new_ssh_key_email.text.erb
index 05b551c89a0..690357d69ed 100644
--- a/app/views/notify/new_ssh_key_email.text.erb
+++ b/app/views/notify/new_ssh_key_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
A new public key was added to your account:
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index db4424a01f9..dfbb5c75bd3 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -1,5 +1,5 @@
%p
- Hi #{@user['name']}!
+ Hi #{sanitize_name(@user['name'])}!
%p
- if Gitlab::CurrentSettings.allow_signup?
Your account has been created successfully.
diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb
index dd9b71e3b84..f3f20f3bfba 100644
--- a/app/views/notify/new_user_email.text.erb
+++ b/app/views/notify/new_user_email.text.erb
@@ -1,4 +1,4 @@
-Hi <%= @user.name %>!
+Hi <%= sanitize_name(@user.name) %>!
The Administrator created an account for you. Now you are a member of the company GitLab application.
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index 294238eee51..722eedf90be 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 39622cf7f02..9aadf380f79 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
-Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
-Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
@@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %>
<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
index 67744ec1cee..97258833cfc 100644
--- a/app/views/notify/push_to_merge_request_email.html.haml
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -1,5 +1,5 @@
%h3
- = @updated_by_user.name
+ = sanitize_name(@updated_by_user.name)
pushed new commits to merge request
= link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
index 95759d127e2..10c8e158846 100644
--- a/app/views/notify/push_to_merge_request_email.text.haml
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference}
+#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference}
\
#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
\
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index ee2f40e1683..6d25488a7e2 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -2,7 +2,7 @@
Assignee changed
- if @previous_assignees.any?
from
- %strong= @previous_assignees.map(&:name).to_sentence
+ %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
to
- if @issue.assignees.any?
%strong= @issue.assignee_list
diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb
index 6c357f1074a..7bf2e8e6ce3 100644
--- a/app/views/notify/reassigned_issue_email.text.erb
+++ b/app/views/notify/reassigned_issue_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%>
+Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index 24c2b08810b..e4f19bc3200 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
- %strong= @previous_assignee.name
+ %strong= sanitize_name(@previous_assignee.name)
to
- if @merge_request.assignee_id
- %strong= @merge_request.assignee_name
+ %strong= sanitize_name(@merge_request.assignee_name)
- else
%strong Unassigned
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 998a40fefde..96c770b5219 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
-Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%>
- to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %>
+Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
+ to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
index 522421b7cc3..502b8f21e35 100644
--- a/app/views/notify/resolved_all_discussions_email.html.haml
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -1,2 +1,2 @@
%p
- All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name}
+ All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)}
diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb
index 2881f3e699e..c4b36bfe1a8 100644
--- a/app/views/notify/resolved_all_discussions_email.text.erb
+++ b/app/views/notify/resolved_all_discussions_email.text.erb
@@ -1,3 +1,3 @@
-All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %>
+All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= sanitize_name(@resolved_by.name) %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 65537cf56de..7694217eb28 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,17 +1,17 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
.project-home-panel{ class: ("empty-project" if empty_repo) }
- .project-header.row.append-bottom-8
- .project-title-row.col-md-12.col-lg-6.d-flex
- .avatar-container.project-avatar.float-none
+ .row.append-bottom-8
+ .home-panel-title-row.col-md-12.col-lg-6.d-flex
+ .avatar-container.home-panel-avatar.append-right-default.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.project-title.qa-project-name
+ %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name
= @project.name
- %span.project-visibility.prepend-left-8.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
- .project-metadata.d-flex.align-items-center
+ .home-panel-metadata.d-flex.align-items-center.text-secondary
- if can?(current_user, :read_project, @project)
%span.text-secondary
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -19,7 +19,7 @@
%span.access-request-links.prepend-left-8
= render 'shared/members/access_request_links', source: @project
- if @project.tag_list.present?
- %span.project-topic-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.tag_list.join(', ') : nil }
+ %span.home-panel-topic-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.tag_list.join(', ') : nil }
= sprite_icon('tag', size: 16, css_class: 'icon append-right-4')
= @project.topics_to_show
- if @project.has_extra_topics?
@@ -29,7 +29,7 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
- = render 'projects/buttons/notifications', notification_setting: @notification_setting, btn_class: 'btn-xs'
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs'
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
@@ -44,13 +44,13 @@
- if can?(current_user, :download_code, @project)
%nav.project-stats
- .nav-links.quick-links.mt-3
+ .nav-links.quick-links
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- .project-home-desc.mt-1
+ .home-panel-home-desc.mt-1
- if @project.description.present?
- .project-description
- .project-description-markdown.read-more-container
+ .home-panel-description
+ .home-panel-description-markdown.read-more-container
= markdown_field(@project, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more")
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index d8492abc638..c2329a7aa66 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= icon('info-circle fw')
= succeed '.' do
To learn more about this project, read
- = link_to "the wiki", get_project_wiki_path(viewer.project)
+ = link_to "the wiki", project_wiki_path(viewer.project, :home)
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 44e9cb84341..9d069c025ba 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -25,7 +25,7 @@
= job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
= link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name"
- else
- .light none
+ .light= _('none')
.icon-container.commit-icon
= custom_icon("icon_commit")
@@ -33,10 +33,10 @@
= link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha"
- if job.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
+ = icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
+ = icon('refresh', class: 'text-warning has-tooltip', title: _('Job was retried'))
.label-container
- if job.tags.any?
@@ -44,13 +44,13 @@
%span.badge.badge-primary
= tag
- if job.try(:trigger_request)
- %span.badge.badge-info triggered
+ %span.badge.badge-info= _('triggered')
- if job.try(:allow_failure)
- %span.badge.badge-danger allowed to fail
+ %span.badge.badge-danger= _('allowed to fail')
- if job.schedulable?
%span.badge.badge-info= s_('DelayedJobs|delayed')
- elsif job.action?
- %span.badge.badge-info manual
+ %span.badge.badge-info= _('manual')
- if pipeline_link
%td
@@ -70,7 +70,7 @@
- if job.try(:runner)
= runner_link(job.runner)
- else
- .light none
+ .light= _('none')
- if stage
%td
@@ -97,11 +97,11 @@
%td
.float-right
- if can?(current_user, :read_build, job) && job.artifacts?
- = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
+ = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build' do
= sprite_icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
- = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: 'Cancel', class: 'btn btn-build' do
+ = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif job.scheduled?
.btn-group
@@ -123,8 +123,8 @@
= sprite_icon('time-out')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
- = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
+ = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn btn-build' do
= custom_icon('icon_play')
- elsif job.retryable?
- = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
= icon('repeat')
diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index b4c18374220..59b5b9f8a30 100644
--- a/app/views/projects/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
@@ -1,15 +1,15 @@
- if @status
%p
- %b Status:
- syntax is correct
+ %b= _("Status:")
+ = _("syntax is correct")
%i.fa.fa-ok.correct-syntax
.table-holder
%table.table.table-bordered
%thead
%tr
- %th Parameter
- %th Value
+ %th= _("Parameter")
+ %th= _("Value")
%tbody
- @stages.each do |stage|
- @builds.select { |build| build[:stage] == stage }.each do |build|
@@ -22,27 +22,27 @@
%pre= job[:after_script].to_a.join('\n')
%br
- %b Tag list:
+ %b= _("Tag list:")
= build[:tag_list].to_a.join(", ")
%br
- %b Only policy:
+ %b= _("Only policy:")
= job[:only].to_a.join(", ")
%br
- %b Except policy:
+ %b= _("Except policy:")
= job[:except].to_a.join(", ")
%br
- %b Environment:
+ %b= _("Environment:")
= build[:environment]
%br
- %b When:
+ %b= _("When:")
= build[:when]
- if build[:allow_failure]
- %b Allowed to fail
+ %b= _("Allowed to fail")
- else
%p
- %b Status:
- syntax is incorrect
+ %b= _("Status:")
+ = _("syntax is incorrect")
%i.fa.fa-remove.incorrect-syntax
- %b Error:
+ %b= _("Error:")
= @error
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index cbda6bf2107..7b87664961e 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -1,9 +1,9 @@
-- page_title "CI Lint"
-- page_description "Validate your GitLab CI configuration file"
+- page_title _("CI Lint")
+- page_description _("Validate your GitLab CI configuration file")
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2.pt-3.pb-3 Check your .gitlab-ci.yml
+%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml")
.project-ci-linter
= form_tag project_ci_lint_path(@project), method: :post do
@@ -11,14 +11,14 @@
.col-sm-12
.file-holder
.js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
+ = _("Contents of .gitlab-ci.yml")
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.float-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ = submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
.float-right.prepend-top-10
- = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
+ = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20
.col-sm-12
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f6666921a25..8b6e3e42ea1 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,9 +1,11 @@
+- any_pipelines = @commit.present(current_user: current_user).any_pipelines?
+
%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
= nav_link(path: 'commit#show') do
= link_to project_commit_path(@project, @commit.id) do
Changes
%span.badge.badge-pill= @diffs.size
- - if can?(current_user, :read_pipeline, @project)
+ - if any_pipelines
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_project_commit_path(@project, @commit.id) do
Pipelines
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index a389261136a..90fee2d70be 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -74,8 +74,8 @@
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
= icon('spinner spin')
- - if @commit.last_pipeline
- - last_pipeline = @commit.last_pipeline
+ - last_pipeline = @commit.last_pipeline
+ - if can?(current_user, :read_pipeline, last_pipeline)
.well-segment.pipeline-info
.status-icon-container
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 79e32949db9..06f0cd9675e 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,10 +9,7 @@
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
- - if @commit.status
- = render "ci_menu"
- - else
- .block-connector
+ = render "ci_menu"
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
.limited-width-notes
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1a74b120c26..0d3c6e7027c 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,6 +6,7 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- commit_status = commit.present(current_user: current_user).status_for(ref)
- link = commit_path(project, commit, merge_request: merge_request)
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -22,7 +23,7 @@
%span.commit-row-message.d-block.d-sm-none
&middot;
= commit.short_id
- - if commit.status(ref)
+ - if commit_status
.d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
@@ -45,7 +46,7 @@
- else
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
- - if commit.status(ref)
+ - if commit_status
= render_commit_status(commit, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 062aa423bde..24d665761cc 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index c73d167303f..310e339ac8d 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -12,6 +12,7 @@
%ul.content-list.related-items-list
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
+ - merge_request = merge_request.present(current_user: current_user)
%li.list-item.py-0.px-0
.item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
.item-contents
@@ -25,7 +26,7 @@
= merge_request.target_project.full_path
= merge_request.to_reference
%span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- - if merge_request.head_pipeline
+ - if merge_request.can_read_pipeline?
= render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom')
- elsif has_any_head_pipeline
= icon('blank fw')
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 1df38db9fd4..ffdd96870ef 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -6,7 +6,7 @@
%li
- target = @project.repository.find_branch(branch).dereferenced_target
- pipeline = @project.pipeline_for(branch, target.sha) if target
- - if pipeline
+ - if can?(current_user, :read_pipeline, pipeline)
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
%span.related-branch-info
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f048fb91304..653b7d4c6f3 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -15,7 +15,10 @@
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
= sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block
- Closed
+ - if @issue.moved?
+ = _("Closed (moved)")
+ - else
+ = _("Closed")
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
= sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block Open
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 02d2dbf0d61..ac29cd8f679 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -46,7 +46,7 @@
%li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
CLOSED
- - if merge_request.head_pipeline
+ - if can?(current_user, :read_pipeline, merge_request.head_pipeline)
%li.issuable-pipeline-status.d-none.d-sm-inline-block
= render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 56c043b2b3d..19f5bba75c4 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -5,23 +5,25 @@
.row
.col-md-6
.form-group.row
- = f.label :title, "Title", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :title, _('Title')
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "qa-milestone-title form-control", required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: 'qa-milestone-title form-control', required: true, autofocus: true
.form-group.row.milestone-description
- = f.label :description, "Description", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :description, _('Description')
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: 'Write milestone description...'
+ = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: _('Write milestone description...')
= render 'shared/notes/hints'
.clearfix
.error-alert
- = render "shared/milestones/form_dates", f: f
+ = render 'shared/milestones/form_dates', f: f
.form-actions
- if @milestone.new_record?
- = f.submit 'Create milestone', class: "btn-success btn qa-milestone-create-button"
- = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel"
+ = f.submit _('Create milestone'), class: 'btn-create btn qa-milestone-create-button'
+ = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel'
- else
- = f.submit 'Save changes', class: "btn-success btn"
- = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel"
+ = f.submit _('Save changes'), class: 'btn-success btn'
+ = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'btn btn-cancel'
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 4006a468792..aa564e00af9 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,14 +1,14 @@
- @no_container = true
-- breadcrumb_title "Edit"
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
-- page_title "Edit", @milestone.title, "Milestones"
+- breadcrumb_title _('Edit')
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
+- page_title _('Edit'), @milestone.title, _('Milestones')
%div{ class: container_class }
%h3.page-title
- Edit Milestone
+ = _('Edit Milestone')
%hr
- = render "form"
+ = render 'form'
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 57f3c640696..a3414c16d73 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,15 +1,16 @@
- @no_container = true
-- page_title 'Milestones'
+- page_title _('Milestones')
%div{ class: container_class }
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
.nav-controls
+ = render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do
- New milestone
+ = link_to new_project_milestone_path(@project), class: 'btn btn-success qa-new-project-milestone', title: _('New milestone') do
+ = _('New milestone')
.milestones
#delete-milestone-modal
@@ -20,6 +21,6 @@
- if @milestones.blank?
%li
- .nothing-here-block No milestones to show
+ .nothing-here-block= _('No milestones to show')
= paginate @milestones, theme: 'gitlab'
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 01cc951e8c2..79207fd70b5 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,12 +1,12 @@
- @no_container = true
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
-- breadcrumb_title "New"
-- page_title "New Milestone"
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
+- breadcrumb_title _('New')
+- page_title _('New Milestone')
%div{ class: container_class }
%h3.page-title
- New Milestone
+ = _('New Milestone')
%hr
- = render "form"
+ = render 'form'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5859de61d71..0542b349e44 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,30 +1,30 @@
- @no_container = true
-- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
+- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
- breadcrumb_title @milestone.title
-- page_title @milestone.title, "Milestones"
+- page_title @milestone.title, _('Milestones')
- page_description @milestone.description
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
- Closed
+ = _('Closed')
- elsif @milestone.expired?
- Past due
+ = _('Past due')
- elsif @milestone.upcoming?
- Upcoming
+ = _('Upcoming')
- else
- Open
+ = _('Open')
.header-text-content
%span.identifier
%strong
- Milestone
+ = _('Milestone')
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
- if can?(current_user, :admin_milestone, @project)
- = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do
- Edit
+ = link_to edit_project_milestone_path(@project, @milestone), class: 'btn btn-grouped btn-nr' do
+ = _('Edit')
- if @project.group
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
@@ -39,13 +39,13 @@
#promote-milestone-modal
- if @milestone.active?
- = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
+ = link_to _('Close milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: 'btn btn-close btn-nr btn-grouped'
- else
- = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
+ = link_to _('Reopen milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: 'btn btn-reopen btn-nr btn-grouped'
= render 'shared/milestones/delete_button'
- %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: '#' }
= icon('angle-double-left')
.detail-page-description.milestone-detail
@@ -62,10 +62,10 @@
- if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
- %span Assign some issues to this milestone.
+ %span= _('Assign some issues to this milestone.')
- elsif @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
- %span All issues for this milestone are closed. You may close this milestone now.
+ %span= _('All issues for this milestone are closed. You may close this milestone now.')
= render 'deprecation_message'
= render 'shared/milestones/tabs', milestone: @milestone
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index 0d848f7899c..b7b46c56c37 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -6,25 +6,24 @@
.form-group.row
= f.label :domain, class: 'col-form-label col-sm-2' do
- Domain
+ = _("Domain")
.col-sm-10
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
.form-group.row
= f.label :certificate, class: 'col-form-label col-sm-2' do
- Certificate (PEM)
+ = _("Certificate (PEM)")
.col-sm-10
= f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline Upload a certificate for your domain with all intermediates
+ %span.help-inline= _("Upload a certificate for your domain with all intermediates")
.form-group.row
= f.label :key, class: 'col-form-label col-sm-2' do
- Key (PEM)
+ = _("Key (PEM)")
.col-sm-10
= f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline Upload a private key for your certificate
+ %span.help-inline= _("Upload a private key for your certificate")
- else
.nothing-here-block
- Support for custom certificates is disabled.
- Ask your system's administrator to enable it.
+ = _("Support for custom certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index 342b1482df7..e11387ae742 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -1,4 +1,4 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
- page_title @domain.domain
%h3.page-title
@@ -8,4 +8,4 @@
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Save Changes', class: "btn btn-success"
+ = f.submit _('Save Changes'), class: "btn btn-success"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 94ad1470052..c7cefa87c76 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,12 +1,12 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
-- page_title 'New Pages Domain'
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
+- page_title _('New Pages Domain')
%h3.page-title
- New Pages Domain
+ = _("New Pages Domain")
%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
- = f.submit 'Create New Domain', class: "btn btn-success"
+ = f.submit _('Create New Domain'), class: "btn btn-success"
.float-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index a8484187493..82147568981 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,6 +1,6 @@
-- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
-- page_title "#{@domain.domain}", 'Pages Domains'
+- page_title "#{@domain.domain}", _('Pages Domains')
- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}."
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
@@ -9,37 +9,37 @@
= content_for :flash_message do
.alert.alert-warning
.container-fluid.container-limited
- This domain is not verified. You will need to verify ownership before access is enabled.
+ = _("This domain is not verified. You will need to verify ownership before access is enabled.")
%h3.page-title.with-button
- = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
- Pages Domain
+ = link_to _('Edit'), edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
+ = _("Pages Domain")
.table-holder
%table.table
%tr
%td
- Domain
+ = _("Domain")
%td
= link_to @domain.url do
= @domain.url
= icon('external-link')
%tr
%td
- DNS
+ = _("DNS")
%td
.input-group
= text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
.input-group-append
= clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
%p.form-text.text-muted
- To access this domain create a new DNS record
+ = _("To access this domain create a new DNS record")
- if verification_enabled
- verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
%tr
%td
- Verification status
+ = _("Verification status")
%td
= form_tag verify_project_pages_domain_path(@project, @domain) do
.status-badge
@@ -53,17 +53,16 @@
.input-group-append
= clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
%p.form-text.text-muted
- - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
- To #{link_to 'verify ownership', help_link} of your domain,
- add the above key to a TXT record within to your DNS configuration.
+ - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record'))
+ = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help }
%tr
%td
- Certificate
+ = _("Certificate")
%td
- if @domain.certificate_text
%pre
= @domain.certificate_text
- else
.light
- missing
+ = _("missing")
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 0f0114d513c..69a47faabed 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -6,23 +6,22 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
- - if commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.total_size, "job"
- - if @pipeline.ref
- from
- - if @pipeline.ref_exists?
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - else
- %span.ref-name
- = @pipeline.ref
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ - if @pipeline.ref_exists?
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - else
+ %span.ref-name
+ = @pipeline.ref
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
.well-segment
.icon-container
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index bb328f5344c..bfb275b9ef5 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -110,6 +110,9 @@
%li
go test -cover (Go)
%code coverage: \d+.\d+% of statements
+ %li
+ nyc npm test (NodeJS) -
+ %code All files[^|]*\|[^|]*\s+([\d\.]+)
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index f55202c2c5f..cc203cfad86 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -28,7 +28,7 @@
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
+ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 15a960f81b8..feeaf799f51 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -20,7 +20,7 @@
.nav-controls.controls-flex
- if can?(current_user, :push_code, @project)
- = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
+ = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
= link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do
= icon('files-o')
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 7e4618e1a88..6f6f1e5e0c5 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- - if can?(current_user, :admin_trigger, trigger)
+ - if trigger.has_token_exposed?
%span= trigger.token
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
- else
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 7d8826e540c..d1556dbd077 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -16,7 +16,7 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control', value: @page.title
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- if @page.persisted?
%span.edit-wiki-page-slug-tip
= icon('lightbulb-o')
@@ -31,7 +31,7 @@
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
- = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
+ = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files here…")
= render 'shared/notes/hints'
.clearfix
@@ -47,14 +47,14 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
- = f.submit _("Save changes"), class: 'btn-success btn'
+ = f.submit _("Save changes"), class: 'btn-success btn qa-save-changes-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
- = f.submit s_("Wiki|Create page"), class: 'btn-success btn'
+ = f.submit s_("Wiki|Create page"), class: 'btn-success btn qa-create-page-button'
.float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index aeef64fd7eb..94267b6e0cf 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
+- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 4d5fd55364c..8b348bb4e4f 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.human_title, _("Wiki")
-- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
+- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index df3308abe0d..73eedcc1dc9 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -2,7 +2,7 @@
- if can?(current_user, :create_wiki, @project)
- create_path = project_wiki_path(@project, params[:id], { view: 'create' })
- - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page')
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
index a5f100e3469..d44017299b8 100644
--- a/app/views/shared/empty_states/_wikis_layout.html.haml
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -1,6 +1,6 @@
.row.empty-state
.col-12
- .svg-content
+ .svg-content.qa-svg-content
= image_tag image_path
.col-12
.text-content.text-center
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 20847378495..588659c7e9c 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -57,10 +57,10 @@
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
@@ -73,16 +73,16 @@
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
- %li.filter-dropdown-item{ data: { value: 'upcoming' } }
+ %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
@@ -92,10 +92,10 @@
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
@@ -107,10 +107,10 @@
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
%button.btn.btn-link{ type: 'button' }
= _('None')
- %li.filter-dropdown-item{ data: { value: 'any' } }
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
deleted file mode 100644
index ebae58f28ba..00000000000
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-- model_name = source.model_name.to_s.downcase
-
-- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord
- .project-action-button.inline
- - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
- = link_to link_text, polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: leave_confirmation_message(source) },
- class: 'btn'
-- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
- .project-action-button.inline
- = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(requester) },
- class: 'btn'
-- elsif source.request_access_enabled && can?(current_user, :request_access, source)
- .project-action-button.inline
- = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn'
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 922805958a5..4de89d7c7a0 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,11 +1,13 @@
.col-md-6
.form-group.row
- = f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :start_date, "Start Date"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.form-group.row
- = f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
+ .col-form-label.col-sm-2
+ = f.label :due_date, "Due Date"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
%a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/milestones/_search_form.html.haml b/app/views/shared/milestones/_search_form.html.haml
new file mode 100644
index 00000000000..403a0224a85
--- /dev/null
+++ b/app/views/shared/milestones/_search_form.html.haml
@@ -0,0 +1,8 @@
+= form_tag request.path, method: :get do |f|
+ = search_field_tag :search_title, params[:search_title],
+ placeholder: _('Filter by milestone name'),
+ class: 'form-control input-short',
+ spellcheck: false
+ = hidden_field_tag :state, params[:state]
+ = hidden_field_tag :sort, params[:sort]
+
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 30860988bbb..2ece7b7f701 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,7 +1,7 @@
- btn_class = local_assigns.fetch(:btn_class, nil)
- if notification_setting
- .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
+ .js-notification-dropdown.notification-dropdown.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/shared/notifications/_new_button.html.haml
index a8b728527c8..6d26dbebbc8 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -1,7 +1,7 @@
-- btn_class = local_assigns.fetch(:btn_class, "btn-xs")
+- btn_class = local_assigns.fetch(:btn_class, nil)
- if notification_setting
- .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline
+ .js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
= hidden_setting_source_input(notification_setting)
= hidden_field_tag "hide_label", true
@@ -9,14 +9,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
%span.js-notification-loading.fa.hidden
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("arrow-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
%span.js-notification-loading.fa.hidden
= sprite_icon("arrow-down", css_class: "icon")
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index fea7e17be3d..e1564d57426 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -84,7 +84,7 @@
title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count)
- - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
+ - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
%span.icon-wrapper.pipeline-status
= render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
.updated-note
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 8da63a29ca6..211e3eafac6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -74,11 +74,11 @@
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider
- = icon('map-marker')
+ = sprite_icon('location', size: 16, css_class: 'vertical-align-sub')
= @user.location
- unless @user.organization.blank?
.profile-link-holder.middle-dot-divider
- = icon('briefcase')
+ = sprite_icon('work', size: 16, css_class: 'vertical-align-sub')
= @user.organization
- if @user.bio.present?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 223ddc80c88..85c123c2704 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -45,6 +45,8 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- hashed_storage:hashed_storage_migrator
+
- mail_scheduler:mail_scheduler_issue_due
- mail_scheduler:mail_scheduler_notification_service
@@ -90,13 +92,15 @@
- object_pool:object_pool_join
- object_pool:object_pool_destroy
+- container_repository:delete_container_repository
+- container_repository:cleanup_container_repository
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
- authorized_projects
- background_migration
- create_gpg_signature
-- delete_container_repository
- delete_merged_branches
- delete_user
- email_receiver
@@ -129,7 +133,6 @@
- repository_fork
- repository_import
- repository_remove_remote
-- storage_migrator
- system_hook_push
- update_merge_requests
- upload_checksum
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
new file mode 100644
index 00000000000..974ee8c8146
--- /dev/null
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class CleanupContainerRepositoryWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ queue_namespace :container_repository
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :container_repository, :current_user
+
+ def perform(current_user_id, container_repository_id, params)
+ @current_user = User.find_by_id(current_user_id)
+ @container_repository = ContainerRepository.find_by_id(container_repository_id)
+
+ return unless valid?
+
+ try_obtain_lease do
+ Projects::ContainerRepository::CleanupTagsService
+ .new(project, current_user, params)
+ .execute(container_repository)
+ end
+ end
+
+ private
+
+ def valid?
+ current_user && container_repository && project
+ end
+
+ def project
+ container_repository&.project
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_key
+ @lease_key ||= "container_repository:cleanup_tags:#{container_repository.id}"
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ # For ExclusiveLeaseGuard concern
+ def lease_release?
+ # we don't allow to execute this worker
+ # more often than LEASE_TIMEOUT
+ # for given container repository
+ false
+ end
+end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index e8fe9d82797..42e66513ff1 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -4,6 +4,8 @@ class DeleteContainerRepositoryWorker
include ApplicationWorker
include ExclusiveLeaseGuard
+ queue_namespace :container_repository
+
LEASE_TIMEOUT = 1.hour
attr_reader :container_repository
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
new file mode 100644
index 00000000000..49e347d4060
--- /dev/null
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module HashedStorage
+ class MigratorWorker
+ include ApplicationWorker
+
+ queue_namespace :hashed_storage
+
+ # @param [Integer] start initial ID of the batch
+ # @param [Integer] finish last ID of the batch
+ def perform(start, finish)
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_migrate(start: start, finish: finish)
+ end
+ end
+end
diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb
index 4c6339f7701..1c8f313e6e9 100644
--- a/app/workers/project_migrate_hashed_storage_worker.rb
+++ b/app/workers/project_migrate_hashed_storage_worker.rb
@@ -4,21 +4,25 @@ class ProjectMigrateHashedStorageWorker
include ApplicationWorker
LEASE_TIMEOUT = 30.seconds.to_i
+ LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'.freeze
# rubocop: disable CodeReuse/ActiveRecord
def perform(project_id, old_disk_path = nil)
- project = Project.find_by(id: project_id)
- return if project.nil? || project.pending_delete?
-
uuid = lease_for(project_id).try_obtain
+
if uuid
- ::Projects::HashedStorageMigrationService.new(project, old_disk_path || project.full_path, logger: logger).execute
+ project = Project.find_by(id: project_id)
+ return if project.nil? || project.pending_delete?
+
+ old_disk_path ||= project.disk_path
+
+ ::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute
else
- false
+ return false
end
- rescue => ex
+
+ ensure
cancel_lease_for(project_id, uuid) if uuid
- raise ex
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -29,7 +33,8 @@ class ProjectMigrateHashedStorageWorker
private
def lease_key(project_id)
- "project_migrate_hashed_storage_worker:#{project_id}"
+ # we share the same lease key for both migration and rollback so they don't run simultaneously
+ "#{LEASE_KEY_SEGMENT}:#{project_id}"
end
def cancel_lease_for(project_id, uuid)
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
deleted file mode 100644
index fa76fbac55c..00000000000
--- a/app/workers/storage_migrator_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class StorageMigratorWorker
- include ApplicationWorker
-
- def perform(start, finish)
- migrator = Gitlab::HashedStorage::Migrator.new
- migrator.bulk_migrate(start, finish)
- end
-end
diff --git a/bin/secpick b/bin/secpick
index 3d032f696a2..be120a304c9 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -57,8 +57,8 @@ module Secpick
merge_request: {
source_branch: source_branch,
target_branch: security_branch,
- title: "WIP: [#{@options[:version].tr('-', '.')}] ",
- description: '/label ~security'
+ title: "[#{@options[:version].tr('-', '.')}] ",
+ description: '/label ~security ~"Merge into Security"'
}
}
end
diff --git a/changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml b/changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml
new file mode 100644
index 00000000000..5117195cd0c
--- /dev/null
+++ b/changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml
@@ -0,0 +1,5 @@
+---
+title: "Support bamboo api polymorphism"
+merge_request: 24680
+author: Alex Lossent
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/24875-label.yml b/changelogs/unreleased/24875-label.yml
new file mode 100644
index 00000000000..1f9d2222edf
--- /dev/null
+++ b/changelogs/unreleased/24875-label.yml
@@ -0,0 +1,5 @@
+---
+title: Append prioritized label before pagination
+merge_request: 24815
+author:
+type: fixed
diff --git a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml
deleted file mode 100644
index da1777827cb..00000000000
--- a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve Add What's new menu item in top navigation
-merge_request: 23186
-author:
-type: added
diff --git a/changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml b/changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml
new file mode 100644
index 00000000000..70b561ccbf6
--- /dev/null
+++ b/changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml
@@ -0,0 +1,5 @@
+---
+title: Indicate on Issue Status if an Issue was Moved
+merge_request: 24470
+author:
+type: added
diff --git a/changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml b/changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml
new file mode 100644
index 00000000000..01036253151
--- /dev/null
+++ b/changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml
@@ -0,0 +1,5 @@
+---
+title: Fix empty labels of CI builds for gitlab-pages on pipeline page
+merge_request: 24451
+author:
+type: fixed
diff --git a/changelogs/unreleased/44698-recaptcha.yml b/changelogs/unreleased/44698-recaptcha.yml
new file mode 100644
index 00000000000..e1760a6c635
--- /dev/null
+++ b/changelogs/unreleased/44698-recaptcha.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent unload when Recaptcha is open
+merge_request: 24625
+author:
+type: fixed
diff --git a/changelogs/unreleased/45791-number-of-repositories-usage-ping.yml b/changelogs/unreleased/45791-number-of-repositories-usage-ping.yml
new file mode 100644
index 00000000000..8d1f5df56ea
--- /dev/null
+++ b/changelogs/unreleased/45791-number-of-repositories-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Add repositories count to usage ping data
+merge_request: 24823
+author:
+type: added
diff --git a/changelogs/unreleased/50352-sort-save.yml b/changelogs/unreleased/50352-sort-save.yml
new file mode 100644
index 00000000000..cd046c8b785
--- /dev/null
+++ b/changelogs/unreleased/50352-sort-save.yml
@@ -0,0 +1,5 @@
+---
+title: Save issues/merge request sorting options to backend
+merge_request: 24198
+author:
+type: added
diff --git a/changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml b/changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml
new file mode 100644
index 00000000000..cb810b7ac7f
--- /dev/null
+++ b/changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Refresh group overview to match project overview
+merge_request: 23866
+author:
+type: changed
diff --git a/changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml b/changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml
new file mode 100644
index 00000000000..adaaed7f1aa
--- /dev/null
+++ b/changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Display "commented" only for commit discussions on merge requests
+merge_request: 24427
+author:
+type: changed
diff --git a/changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml b/changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml
new file mode 100644
index 00000000000..37dea77b8d2
--- /dev/null
+++ b/changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Standardize filter value capitlization in filter bar in both issues and boards pages
+merge_request: 23846
+author: obahareth
+type: changed
diff --git a/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml b/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml
new file mode 100644
index 00000000000..d1bdbccb20a
--- /dev/null
+++ b/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade kubeclient to 4.2.2 and swap out monkey-patch to disallow redirects
+merge_request: 24284
+author:
+type: other
diff --git a/changelogs/unreleased/54905-milestone-search.yml b/changelogs/unreleased/54905-milestone-search.yml
new file mode 100644
index 00000000000..88717242e7c
--- /dev/null
+++ b/changelogs/unreleased/54905-milestone-search.yml
@@ -0,0 +1,5 @@
+---
+title: Adds milestone search
+merge_request: 24265
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/55820-adds-common-name-chart-value.yml b/changelogs/unreleased/55820-adds-common-name-chart-value.yml
new file mode 100644
index 00000000000..1871abbfc6b
--- /dev/null
+++ b/changelogs/unreleased/55820-adds-common-name-chart-value.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure Cert Manager works with Auto DevOps URLs greater than 64 bytes
+merge_request: 24683
+author:
+type: fixed
diff --git a/changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml b/changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml
new file mode 100644
index 00000000000..ec8a1d9d6ea
--- /dev/null
+++ b/changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Pipeline stages job action button icon is not aligned
+merge_request: 24577
+author:
+type: fixed
diff --git a/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml b/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml
new file mode 100644
index 00000000000..b19b4d650fd
--- /dev/null
+++ b/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix form functionality for edit tag page
+merge_request: 24645
+author:
+type: fixed
diff --git a/changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml b/changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml
new file mode 100644
index 00000000000..089ffd47321
--- /dev/null
+++ b/changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix CSS grid on a new Project/Group Milestone
+merge_request: 24614
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/ab-54270-github-iid.yml b/changelogs/unreleased/ab-54270-github-iid.yml
new file mode 100644
index 00000000000..1776b0aeb86
--- /dev/null
+++ b/changelogs/unreleased/ab-54270-github-iid.yml
@@ -0,0 +1,5 @@
+---
+title: Improve efficiency of GitHub importer by reducing amount of locks needed.
+merge_request: 24102
+author:
+type: performance
diff --git a/changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml b/changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml
new file mode 100644
index 00000000000..a664c44e1d7
--- /dev/null
+++ b/changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml
@@ -0,0 +1,5 @@
+---
+title: Add GitLab Pages predefined CI variables 'CI_PAGES_DOMAIN' and 'CI_PAGES_URL'
+merge_request: 24504
+author: Adrian Moisey
+type: added
diff --git a/changelogs/unreleased/an-opentracing-active-record-tracing.yml b/changelogs/unreleased/an-opentracing-active-record-tracing.yml
new file mode 100644
index 00000000000..59b480675ec
--- /dev/null
+++ b/changelogs/unreleased/an-opentracing-active-record-tracing.yml
@@ -0,0 +1,5 @@
+---
+title: Adds tracing support for ActiveRecord notifications
+merge_request: 24604
+author:
+type: other
diff --git a/changelogs/unreleased/an-opentracing-render-tracing.yml b/changelogs/unreleased/an-opentracing-render-tracing.yml
new file mode 100644
index 00000000000..6ff7f1f3cf2
--- /dev/null
+++ b/changelogs/unreleased/an-opentracing-render-tracing.yml
@@ -0,0 +1,5 @@
+---
+title: Add OpenTracing instrumentation for Action View Render events
+merge_request: 24728
+author:
+type: other
diff --git a/changelogs/unreleased/cluster_status_for_ugprading.yml b/changelogs/unreleased/cluster_status_for_ugprading.yml
new file mode 100644
index 00000000000..ca1f8b3a786
--- /dev/null
+++ b/changelogs/unreleased/cluster_status_for_ugprading.yml
@@ -0,0 +1,5 @@
+---
+title: Expose version for each application in cluster_status JSON endpoint
+merge_request: 24791
+author:
+type: other
diff --git a/changelogs/unreleased/container-repository-cleanup-api.yml b/changelogs/unreleased/container-repository-cleanup-api.yml
new file mode 100644
index 00000000000..c2b23a9add0
--- /dev/null
+++ b/changelogs/unreleased/container-repository-cleanup-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add Container Registry API with cleanup function
+merge_request: 24303
+author:
+type: added
diff --git a/changelogs/unreleased/dm-copy-suggestion-as-gfm.yml b/changelogs/unreleased/dm-copy-suggestion-as-gfm.yml
new file mode 100644
index 00000000000..96115e6ade1
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-suggestion-as-gfm.yml
@@ -0,0 +1,5 @@
+---
+title: Allow suggestions to be copied and pasted as GFM
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml b/changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml
new file mode 100644
index 00000000000..3d87807dbc1
--- /dev/null
+++ b/changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust vertical alignment for project visibility icons
+merge_request: 24511
+author: Martin Hobert
+type: fixed
diff --git a/changelogs/unreleased/fix-49388.yml b/changelogs/unreleased/fix-49388.yml
new file mode 100644
index 00000000000..f8b5e3e1943
--- /dev/null
+++ b/changelogs/unreleased/fix-49388.yml
@@ -0,0 +1,5 @@
+---
+title: Update metrics environment dropdown to show complete option set
+merge_request: 24441
+author:
+type: fixed
diff --git a/changelogs/unreleased/gt-externalize-app-views-clusters.yml b/changelogs/unreleased/gt-externalize-app-views-clusters.yml
new file mode 100644
index 00000000000..6d2284ead37
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/clusters`
+merge_request: 24666
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml b/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml
new file mode 100644
index 00000000000..ecc878ab892
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/projects/ci`
+merge_request: 24617
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml b/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml
new file mode 100644
index 00000000000..56aaac812bb
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/projects/milestones`
+merge_request: 24726
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml b/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml
new file mode 100644
index 00000000000..f60776a2ed8
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/projects/pages_domains`
+merge_request: 24723
+author: George Tsiolis
+type: other
diff --git a/changelogs/unreleased/hnk-master-patch-61932.yml b/changelogs/unreleased/hnk-master-patch-61932.yml
new file mode 100644
index 00000000000..8cc9d0057a9
--- /dev/null
+++ b/changelogs/unreleased/hnk-master-patch-61932.yml
@@ -0,0 +1,5 @@
+---
+title: Update runner admin page to make description field larger
+merge_request: 23593
+author: Sascha Reynolds
+type: fixed
diff --git a/changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml b/changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml
new file mode 100644
index 00000000000..3ba62b92413
--- /dev/null
+++ b/changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml
@@ -0,0 +1,5 @@
+---
+title: Adjusts suggestions unable to be applied
+merge_request: 24603
+author:
+type: fixed
diff --git a/changelogs/unreleased/patch-38.yml b/changelogs/unreleased/patch-38.yml
new file mode 100644
index 00000000000..9179fc6846e
--- /dev/null
+++ b/changelogs/unreleased/patch-38.yml
@@ -0,0 +1,5 @@
+---
+title: fix display comment avatars issue in IE 11
+merge_request: 24777
+author: Gokhan Apaydin
+type: fixed
diff --git a/changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml b/changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml
new file mode 100644
index 00000000000..98859e8aa07
--- /dev/null
+++ b/changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml
@@ -0,0 +1,5 @@
+---
+title: Refactored NoteableDiscussion by extracting ResolveDiscussionButton
+merge_request: 24505
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml b/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml
new file mode 100644
index 00000000000..9a0d16c2d70
--- /dev/null
+++ b/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml
@@ -0,0 +1,5 @@
+---
+title: Extracted JumpToNextDiscussionButton to its own component
+author: Martin Hobert
+merge_request: 24506
+type: other
diff --git a/changelogs/unreleased/remove-diff-coloring.yml b/changelogs/unreleased/remove-diff-coloring.yml
new file mode 100644
index 00000000000..1ee1b525c35
--- /dev/null
+++ b/changelogs/unreleased/remove-diff-coloring.yml
@@ -0,0 +1,5 @@
+---
+title: 'remove red/green colors from diff view of no-color syntax theme'
+merge_request: 24582
+author: khm
+type: changed
diff --git a/changelogs/unreleased/security-22076-sanitize-url-in-names.yml b/changelogs/unreleased/security-22076-sanitize-url-in-names.yml
new file mode 100644
index 00000000000..4e0ad4dd4c4
--- /dev/null
+++ b/changelogs/unreleased/security-22076-sanitize-url-in-names.yml
@@ -0,0 +1,6 @@
+---
+title: Sanitize user full name to clean up any URL to prevent mail clients from auto-linking
+ URLs
+merge_request: 2793
+author:
+type: security
diff --git a/changelogs/unreleased/security-55320-stored-xss-in-user-status.yml b/changelogs/unreleased/security-55320-stored-xss-in-user-status.yml
new file mode 100644
index 00000000000..8ea9ae0ccdf
--- /dev/null
+++ b/changelogs/unreleased/security-55320-stored-xss-in-user-status.yml
@@ -0,0 +1,5 @@
+---
+title: Use sanitized user status message for user popover
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-stored-xss-via-katex.yml b/changelogs/unreleased/security-stored-xss-via-katex.yml
new file mode 100644
index 00000000000..a71ae1123f2
--- /dev/null
+++ b/changelogs/unreleased/security-stored-xss-via-katex.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed XSS content in KaTex links
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml b/changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml
new file mode 100644
index 00000000000..5af3bdce51b
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix failed LDAP logins when nil user_id present
+merge_request: 24749
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml b/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml
new file mode 100644
index 00000000000..addf327b69d
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml
@@ -0,0 +1,5 @@
+---
+title: Alias GitHub and BitBucket OAuth2 callback URLs
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-pages-zip-constant.yml b/changelogs/unreleased/sh-fix-pages-zip-constant.yml
new file mode 100644
index 00000000000..fcd8aa45825
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-pages-zip-constant.yml
@@ -0,0 +1,5 @@
+---
+title: Fix uninitialized constant with GitLab Pages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-issue-53419-fix.yml b/changelogs/unreleased/sh-issue-53419-fix.yml
new file mode 100644
index 00000000000..ab8b65857e2
--- /dev/null
+++ b/changelogs/unreleased/sh-issue-53419-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Bitbucket Server import not allowing personal projects
+merge_request: 23601
+author:
+type: fixed
diff --git a/changelogs/unreleased/test-permissions.yml b/changelogs/unreleased/test-permissions.yml
new file mode 100644
index 00000000000..cfb69fdcb1e
--- /dev/null
+++ b/changelogs/unreleased/test-permissions.yml
@@ -0,0 +1,5 @@
+---
+title: Disallows unauthorized users from accessing the pipelines section.
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml b/changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml
new file mode 100644
index 00000000000..32259bfacd4
--- /dev/null
+++ b/changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Update to GitLab SVG icon from Font Awesome in profile for location and work
+merge_request: 24671
+author: Yoginth
+type: changed
diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb
deleted file mode 100644
index f8fe1156aaa..00000000000
--- a/config/initializers/kubeclient.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-class Kubeclient::Client
- # 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/initializers/tracing.rb b/config/initializers/tracing.rb
index 46e8125daf8..ddd91150c90 100644
--- a/config/initializers/tracing.rb
+++ b/config/initializers/tracing.rb
@@ -23,6 +23,10 @@ if Gitlab::Tracing.enabled?
end
end
+ # Instrument Rails
+ Gitlab::Tracing::Rails::ActiveRecordSubscriber.instrument
+ Gitlab::Tracing::Rails::ActionViewSubscriber.instrument
+
# In multi-processed clustered architectures (puma, unicorn) don't
# start tracing until the worker processes are spawned. This works
# around issues when the opentracing implementation spawns threads
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 3998d977c81..69df82611f2 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -1,3 +1,12 @@
+# Alias import callbacks under the /users/auth endpoint so that
+# the OAuth2 callback URL can be restricted under http://example.com/users/auth
+# instead of http://example.com.
+Devise.omniauth_providers.each do |provider|
+ next if provider == 'ldapmain'
+
+ get "/users/auth/-/import/#{provider}/callback", to: "import/#{provider}#callback", as: "users_import_#{provider}_callback"
+end
+
namespace :import do
resource :github, only: [:create, :new], controller: :github do
post :personal_access_token
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 3e8c218052d..1e094c03171 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -47,7 +47,6 @@
- [project_service, 1]
- [delete_user, 1]
- [todos_destroyer, 1]
- - [delete_container_repository, 1]
- [delete_merged_branches, 1]
- [authorized_projects, 1]
- [expire_build_instance_artifacts, 1]
@@ -69,7 +68,7 @@
- [background_migration, 1]
- [gcp_cluster, 1]
- [project_migrate_hashed_storage, 1]
- - [storage_migrator, 1]
+ - [hashed_storage, 1]
- [pages_domain_verification, 1]
- [object_storage_upload, 1]
- [object_storage, 1]
@@ -81,6 +80,7 @@
- [delete_diff_files, 1]
- [detect_repository_languages, 1]
- [auto_devops, 2]
+ - [container_repository, 1]
- [object_pool, 1]
- [repository_cleanup, 1]
- [delete_stored_files, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b9044e13f50..fdf179b007a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -94,6 +94,9 @@ module.exports = {
vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
vue$: 'vue/dist/vue.esm.js',
spec: path.join(ROOT_PATH, 'spec/javascripts'),
+
+ // the following resolves files which are different between CE and EE
+ ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
},
},
diff --git a/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb
new file mode 100644
index 00000000000..7bf581fe9b0
--- /dev/null
+++ b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSortingFieldsToUserPreference < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column :user_preferences, :issues_sort, :string
+ add_column :user_preferences, :merge_requests_sort, :string
+ end
+
+ def down
+ remove_column :user_preferences, :issues_sort
+ remove_column :user_preferences, :merge_requests_sort
+ end
+end
diff --git a/db/post_migrate/20181219130552_update_project_import_visibility_level.rb b/db/post_migrate/20181219130552_update_project_import_visibility_level.rb
new file mode 100644
index 00000000000..6209de88b31
--- /dev/null
+++ b/db/post_migrate/20181219130552_update_project_import_visibility_level.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class UpdateProjectImportVisibilityLevel < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ BATCH_SIZE = 100
+
+ PRIVATE = 0
+ INTERNAL = 10
+
+ disable_ddl_transaction!
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ end
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ belongs_to :namespace
+
+ IMPORT_TYPE = 'gitlab_project'
+
+ scope :with_group_visibility, ->(visibility) do
+ joins(:namespace)
+ .where(namespaces: { type: 'Group', visibility_level: visibility })
+ .where(import_type: IMPORT_TYPE)
+ .where('projects.visibility_level > namespaces.visibility_level')
+ end
+
+ self.table_name = 'projects'
+ end
+
+ def up
+ # Update project's visibility to be the same as the group
+ # if it is more restrictive than `PUBLIC`.
+ update_projects_visibility(PRIVATE)
+ update_projects_visibility(INTERNAL)
+ end
+
+ def down
+ # no-op: unrecoverable data migration
+ end
+
+ private
+
+ def update_projects_visibility(visibility)
+ say_with_time("Updating project visibility to #{visibility} on #{Project::IMPORT_TYPE} imports.") do
+ Project.with_group_visibility(visibility).select(:id).each_batch(of: BATCH_SIZE) do |batch, _index|
+ batch_sql = Gitlab::Database.mysql? ? batch.pluck(:id).join(', ') : batch.select(:id).to_sql
+
+ say("Updating #{batch.size} items.", true)
+
+ execute("UPDATE projects SET visibility_level = '#{visibility}' WHERE id IN (#{batch_sql})")
+ end
+ end
+ end
+end
diff --git a/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb b/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb
new file mode 100644
index 00000000000..ddcddcf72a3
--- /dev/null
+++ b/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+class DeleteInconsistentInternalIdRecords2 < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # This migration cleans up any inconsistent records in internal_ids.
+ #
+ # That is, it deletes records that track a `last_value` that is
+ # smaller than the maximum internal id (usually `iid`) found in
+ # the corresponding model records.
+
+ def up
+ disable_statement_timeout do
+ delete_internal_id_records('milestones', 'project_id')
+ delete_internal_id_records('milestones', 'namespace_id', 'group_id')
+ end
+ end
+
+ class InternalId < ActiveRecord::Base
+ self.table_name = 'internal_ids'
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 }
+ end
+
+ private
+
+ def delete_internal_id_records(base_table, scope_column_name, base_scope_column_name = scope_column_name)
+ sql = <<~SQL
+ SELECT id FROM ( -- workaround for MySQL
+ SELECT internal_ids.id FROM (
+ SELECT #{base_scope_column_name} AS #{scope_column_name}, max(iid) as maximum_iid from #{base_table} GROUP BY #{scope_column_name}
+ ) maxima JOIN internal_ids USING (#{scope_column_name})
+ WHERE internal_ids.usage=#{InternalId.usages.fetch(base_table)} AND maxima.maximum_iid > internal_ids.last_value
+ ) internal_ids
+ SQL
+
+ InternalId.where("id IN (#{sql})").tap do |ids| # rubocop:disable GitlabSecurity/SqlInjection
+ say "Deleting internal_id records for #{base_table}: #{ids.map { |i| [i.project_id, i.last_value] }}" unless ids.empty?
+ end.delete_all
+ end
+end
diff --git a/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb b/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb
new file mode 100644
index 00000000000..4fcee326b7e
--- /dev/null
+++ b/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class MigrateDeleteContainerRepositoryWorker < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ sidekiq_queue_migrate('delete_container_repository', to: 'container_repository:delete_container_repository')
+ end
+
+ def down
+ sidekiq_queue_migrate('container_repository:delete_container_repository', to: 'delete_container_repository')
+ end
+end
diff --git a/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb b/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb
new file mode 100644
index 00000000000..193bd571831
--- /dev/null
+++ b/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateStorageMigratorSidekiqQueue < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ sidekiq_queue_migrate 'storage_migrator', to: 'hashed_storage:hashed_storage_migrator'
+ end
+
+ def down
+ sidekiq_queue_migrate 'hashed_storage:hashed_storage_migrator', to: 'storage_migrator'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cd502d06bf4..7c1733becb9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190115054216) do
+ActiveRecord::Schema.define(version: 20190124200344) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -2146,6 +2146,8 @@ ActiveRecord::Schema.define(version: 20190115054216) do
t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
+ t.string "issues_sort"
+ t.string "merge_requests_sort"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
diff --git a/doc/README.md b/doc/README.md
index b15c3a63d92..1a0359f9e2a 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -52,6 +52,11 @@ GitLab provides solutions for [all the stages of the DevOps lifecycle](https://a
![DevOps Stages](img/devops-stages.png)
+GitLab is like a top-of-the-line kitchen for making software. As the executive
+chef, you decide what software you want serve. Using your recipe, GitLab handles
+all the prep work, cooking, and delivery, so you can turn around orders faster
+than ever.
+
The following sections provide links to documentation for each DevOps stage:
| DevOps Stage | Documentation for |
diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md
index 341a00009e5..11b2adeeeb8 100644
--- a/doc/administration/git_protocol.md
+++ b/doc/administration/git_protocol.md
@@ -5,6 +5,13 @@ description: "Set and configure Git protocol v2"
# Configuring Git Protocol v2
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46555) in GitLab 11.4.
+> [Temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+
+
+NOTE: **Note:**
+Git protocol v2 support has been [temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769),
+as a feature used to hide certain internal references does not function when it
+is enabled, and this has a security impact. Once this problem has been resolved,
+protocol v2 support will be re-enabled.
Git protocol v2 improves the v1 wire protocol in several ways and is
enabled by default in GitLab for HTTP requests. In order to enable SSH,
diff --git a/doc/administration/index.md b/doc/administration/index.md
index ecb0801bac4..0b673d61139 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -89,7 +89,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
-- [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
+- [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers. See also other [authentication](../topics/authentication/index.md#gitlab-administrators) topics (for example, enforcing 2FA).
- [Incoming email](incoming_email.md): Configure incoming emails to allow
users to [reply by email], create [issues by email] and
[merge requests by email], and to enable [Service Desk].
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index cbd3032bd4e..10ae8c7dedf 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -332,6 +332,42 @@ The maximum size of the unpacked archive per project can be configured in the
Admin area under the Application settings in the **Maximum size of pages (MB)**.
The default is 100MB.
+## Running GitLab Pages in a separate server
+
+You may want to run GitLab Pages daemon on a separate server in order to decrease the load on your main application server.
+Follow the steps below to configure GitLab Pages in a separate server.
+
+1. Suppose you have the main GitLab application server named `app1`. Prepare
+new Linux server (let's call it `app2`), create NFS share there and configure access to
+this share from `app1`. Let's use the default GitLab Pages folder `/var/opt/gitlab/gitlab-rails/shared/pages`
+as the shared folder on `app2` and mount it to `/mnt/pages` on `app1`.
+
+1. On `app2` install GitLab omnibus and modify `/etc/gitlab/gitlab.rb` this way:
+
+ ```shell
+ external_url 'http://<ip-address-of-the-server>'
+ pages_external_url "http://<your-pages-domain>"
+ postgresql['enable'] = false
+ redis['enable'] = false
+ prometheus['enable'] = false
+ unicorn['enable'] = false
+ sidekiq['enable'] = false
+ gitlab_workhorse['enable'] = false
+ gitaly['enable'] = false
+ alertmanager['enable'] = false
+ node_exporter['enable'] = false
+ ```
+1. Run `sudo gitlab-ctl reconfigure`.
+1. On `app1` apply the following changes to `/etc/gitlab/gitlab.rb`:
+
+ ```shell
+ gitlab_pages['enable'] = false
+ pages_external_url "http://<your-pages-domain>"
+ gitlab_rails['pages_path'] = "/mnt/pages"
+ ```
+
+1. Run `sudo gitlab-ctl reconfigure`.
+
## Backup
Pages are part of the [regular backup][backup] so there is nothing to configure.
diff --git a/doc/api/README.md b/doc/api/README.md
index 7b83b0fed26..692f63a400c 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -16,6 +16,7 @@ The following API resources are available:
- [Broadcast messages](broadcast_messages.md)
- [Code snippets](snippets.md)
- [Commits](commits.md)
+- [Container Registry](container_registry.md)
- [Custom attributes](custom_attributes.md)
- [Deploy keys](deploy_keys.md), and [deploy keys for multiple projects](deploy_key_multiple_projects.md)
- [Deployments](deployments.md)
@@ -440,7 +441,7 @@ Additional pagination headers are also sent back.
CAUTION: **Caution:**
For performance reasons since
-[GitLab 11.8][https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23931]
+[GitLab 11.8](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23931)
and **behind the `api_kaminari_count_with_limit`
[feature flag](../development/feature_flags.md)**, if the number of resources is
more than 10,000, the `X-Total` and `X-Total-Pages` headers as well as the
@@ -604,7 +605,7 @@ Content-Type: application/json
## Encoding `+` in ISO 8601 dates
If you need to include a `+` in a query parameter, you may need to use `%2B` instead due
-a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that
+to a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that
causes a `+` to be interpreted as a space. For example, in an ISO 8601 date, you may want to pass
a time in Mountain Standard Time, such as:
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
new file mode 100644
index 00000000000..b70854103e8
--- /dev/null
+++ b/doc/api/container_registry.md
@@ -0,0 +1,200 @@
+# Container Registry API
+
+This is the API docs of the [GitLab Container Registry](../user/project/container_registry.md).
+
+## List registry repositories
+
+Get a list of registry repositories in a project.
+
+```
+GET /projects/:id/registry/repositories
+```
+
+| 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. |
+
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "",
+ "path": "group/project",
+ "location": "gitlab.example.com:5000/group/project",
+ "created_at": "2019-01-10T13:38:57.391Z"
+ },
+ {
+ "id": 2,
+ "name": "releases",
+ "path": "group/project/releases",
+ "location": "gitlab.example.com:5000/group/project/releases",
+ "created_at": "2019-01-10T13:39:08.229Z"
+ }
+]
+```
+
+## Delete registry repository
+
+Get a list of repository commits in a project.
+
+This operation is executed asynchronously and might take some time to get executed.
+
+```
+DELETE /projects/:id/registry/repositories/:repository_id
+```
+
+| 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. |
+| `repository_id` | integer | yes | The ID of registry repository. |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2"
+```
+
+## List repository tags
+
+Get a list of tags for given registry repository.
+
+```
+GET /projects/:id/registry/repositories/:repository_id/tags
+```
+
+| 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. |
+| `repository_id` | integer | yes | The ID of registry repository. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "A",
+ "path": "group/project:A",
+ "location": "gitlab.example.com:5000/group/project:A"
+ },
+ {
+ "name": "latest",
+ "path": "group/project:latest",
+ "location": "gitlab.example.com:5000/group/project:latest"
+ }
+]
+```
+
+## Get details of a repository tag
+
+Get details of a registry repository tag.
+
+```
+GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name
+```
+
+| 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. |
+| `repository_id` | integer | yes | The ID of registry repository. |
+| `tag_name` | string | yes | The name of tag. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags/v10.0.0"
+```
+
+Example response:
+
+```json
+{
+ "name": "v10.0.0",
+ "path": "group/project:latest",
+ "location": "gitlab.example.com:5000/group/project:latest",
+ "revision": "e9ed9d87c881d8c2fd3a31b41904d01ba0b836e7fd15240d774d811a1c248181",
+ "short_revision": "e9ed9d87c",
+ "digest": "sha256:c3490dcf10ffb6530c1303522a1405dfaf7daecd8f38d3e6a1ba19ea1f8a1751",
+ "created_at": "2019-01-06T16:49:51.272+00:00",
+ "total_size": 350224384
+}
+```
+
+## Delete a repository tag
+
+Delete a registry repository tag.
+
+```
+DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name
+```
+
+| 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. |
+| `repository_id` | integer | yes | The ID of registry repository. |
+| `tag_name` | string | yes | The name of tag. |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags/v10.0.0"
+```
+
+## Delete repository tags in bulk
+
+Delete repository tags in bulk based on given criteria.
+
+```
+DELETE /projects/:id/registry/repositories/:repository_id/tags
+```
+
+| 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. |
+| `repository_id` | integer | yes | The ID of registry repository. |
+| `name_regex` | string | yes | The regex of the name to delete. To delete all tags specify `.*`. |
+| `keep_n` | integer | no | The amount of latest tags of given name to keep. |
+| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. |
+
+This API call performs the following operations:
+
+1. It orders all tags by creation date. The creation date is the time of the
+ manifest creation, not the time of tag push.
+1. It removes only the tags matching the given `name_regex`.
+1. It never removes the tag named `latest`.
+1. It keeps N latest matching tags (if `keep_n` is specified).
+1. It only removes tags that are older than X amount of time (if `older_than` is specified).
+1. It schedules the asynchronous job to be executed in the background.
+
+These operations are executed asynchronously and it might
+take time to get executed. You can run this at most
+once an hour for a given container repository.
+
+NOTE: **Note:**
+Due to a [Docker Distribution deficiency](https://gitlab.com/gitlab-org/gitlab-ce/issues/21405),
+it doesn't remove tags whose manifest is shared by multiple tags.
+
+Examples:
+
+1. Remove tag names that are matching the regex (Git SHA), keep always at least 5,
+ and remove ones that are older than 2 days:
+
+ ```bash
+ curl --request DELETE --data 'name_regex=[0-9a-z]{40}' --data 'keep_n=5' --data 'older_than=2d' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
+ ```
+
+2. Remove all tags, but keep always the latest 5:
+
+ ```bash
+ curl --request DELETE --data 'name_regex=.*' --data 'keep_n=5' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
+ ```
+
+3. Remove all tags that are older than 1 month:
+
+ ```bash
+ curl --request DELETE --data 'name_regex=.*' --data 'older_than=1month' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags"
+ ```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index fb06119063f..6d8683601f6 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -106,6 +106,8 @@ Example response:
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
"labels" : [],
+ "upvotes": 4,
+ "downvotes": 0,
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/6",
@@ -214,6 +216,8 @@ Example response:
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
+ "upvotes": 4,
+ "downvotes": 0,
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
@@ -327,6 +331,8 @@ Example response:
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
+ "upvotes": 4,
+ "downvotes": 0,
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
@@ -421,6 +427,8 @@ Example response:
"name" : "Dr. Luella Kovacek"
},
"labels" : [],
+ "upvotes": 4,
+ "downvotes": 0,
"id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
@@ -494,6 +502,8 @@ Example response:
"labels" : [
"bug"
],
+ "upvotes": 4,
+ "downvotes": 0,
"author" : {
"name" : "Alexandra Bashirian",
"avatar_url" : null,
@@ -592,6 +602,8 @@ Example response:
"labels" : [
"bug"
],
+ "upvotes": 4,
+ "downvotes": 0,
"id" : 85,
"assignees" : [],
"assignee" : null,
@@ -676,6 +688,8 @@ Example response:
"closed_at": null,
"closed_by": null,
"labels": [],
+ "upvotes": 4,
+ "downvotes": 0,
"milestone": null,
"assignees": [{
"name": "Miss Monserrate Beier",
@@ -758,6 +772,8 @@ Example response:
"closed_at": null,
"closed_by": null,
"labels": [],
+ "upvotes": 4,
+ "downvotes": 0,
"milestone": null,
"assignees": [{
"name": "Miss Monserrate Beier",
@@ -839,6 +855,8 @@ Example response:
"created_at": "2016-04-05T21:41:45.217Z",
"updated_at": "2016-04-07T13:02:37.905Z",
"labels": [],
+ "upvotes": 4,
+ "downvotes": 0,
"milestone": null,
"assignee": {
"name": "Edwardo Grady",
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index c51a3564211..8efb98fe1fc 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -76,7 +76,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project owned by the authenticated user |
-| `cluster_id` | integer | yes | The ID of the cluster |
+| `cluster_id` | integer | yes | The ID of the cluster |
Example request:
@@ -157,12 +157,12 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project owned by the authenticated user |
| `name` | String | yes | The name of the cluster |
-| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true |
-| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API |
+| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true |
+| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes |
-| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
-| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
-| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. |
+| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
+| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
+| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. |
Example request:
@@ -245,11 +245,12 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project owned by the authenticated user |
-| `name` | String | no | The name of the cluster |
-| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API |
-| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
-| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
-| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
+| `cluster_id` | integer | yes | The ID of the cluster |
+| `name` | String | no | The name of the cluster |
+| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API |
+| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
+| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
+| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project |
NOTE: **Note:**
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 9f552a10589..104c64a89ce 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -112,7 +112,7 @@ GET /projects/:id/repository/archive[.format]
```
`format` is an optional suffix for the archive format. Default is
-`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, 'tbz2`, `tb2`,
+`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`,
`bz2`, `tar`, and `zip`. For example, specifying `archive.zip`
would send an archive in ZIP format.
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 495ec099111..8b2ce425cf5 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -29,7 +29,7 @@ needed to compile the project:
Cache was designed to be used to speed up invocations of subsequent runs of a
given job, by keeping things like dependencies (e.g., npm packages, Go vendor
packages, etc.) so they don't have to be re-fetched from the public internet.
- While the cache can be abused to pass intermediate build results between
+ While the cache can be abused to pass intermediate build results between
stages, there may be cases where artifacts are a better fit.
- `artifacts`: **Use for stage results that will be passed between stages.**
Artifacts were designed to upload some compiled/generated bits of the build,
@@ -40,10 +40,10 @@ needed to compile the project:
comply to this rule trigger an unintuitive and illogical error message (an
enhancement is discussed at
[https://gitlab.com/gitlab-org/gitlab-ce/issues/15530](https://gitlab.com/gitlab-org/gitlab-ce/issues/15530)
- ). Artifacts need to be uploaded to the GitLab instance (not only the GitLab
- runner) before the next stage job(s) can start, so you need to evaluate
- carefully whether your bandwidth allows you to profit from parallelization
- with stages and shared artifacts before investing time in changes to the
+ ). Artifacts need to be uploaded to the GitLab instance (not only the GitLab
+ runner) before the next stage job(s) can start, so you need to evaluate
+ carefully whether your bandwidth allows you to profit from parallelization
+ with stages and shared artifacts before investing time in changes to the
setup.
@@ -90,7 +90,7 @@ cache, when declaring `cache` in your jobs, use one or a mix of the following:
that will be only available to a particular project.
- [Use a `key`](../yaml/README.md#cache-key) that fits your workflow (e.g.,
different caches on each branch). For that, you can take advantage of the
- [CI/CD predefined variables](../variables/README.md#predefined-variables-environment-variables).
+ [CI/CD predefined variables](../variables/README.md#predefined-environment-variables).
TIP: **Tip:**
Using the same Runner for your pipeline, is the most simple and efficient way to
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
index aa6b387bc58..f354cdb398e 100644
--- a/doc/ci/docker/using_kaniko.md
+++ b/doc/ci/docker/using_kaniko.md
@@ -40,7 +40,7 @@ In the following example, kaniko is used to build a Docker image and then push
it to [GitLab Container Registry](../../user/project/container_registry.md).
The job will run only when a tag is pushed. A `config.json` file is created under
`/kaniko/.docker` with the needed GitLab Container Registry credentials taken from the
-[environment variables](../variables/README.md#predefined-variables-environment-variables)
+[environment variables](../variables/README.md#predefined-environment-variables)
GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the
root directory of the project, builds the Docker image and pushes it to the
project's Container Registry while tagging it with the Git tag:
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index b9b5ceab7fb..6a9917f6430 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -249,7 +249,7 @@ the basis of [Review apps](review_apps/index.md).
NOTE: **Note:**
The `name` and `url` parameters can use most of the CI/CD variables,
-including [predefined](variables/README.md#predefined-variables-environment-variables),
+including [predefined](variables/README.md#predefined-environment-variables),
[project/group ones](variables/README.md#variables) and
[`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables
defined under `script` or on the Runner's side. There are also other variables that
diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md
index 04b48938e1a..9e657275d50 100644
--- a/doc/ci/examples/artifactory_and_gitlab/index.md
+++ b/doc/ci/examples/artifactory_and_gitlab/index.md
@@ -107,7 +107,7 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-
GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
-First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Variables** page
+First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Environment variables** page
and add the following ones (replace them with your current values, of course):
- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index 68330261910..31c3df81fef 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -22,7 +22,7 @@ container_scanning:
variables:
DOCKER_DRIVER: overlay2
## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables
+ ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
allow_failure: true
@@ -87,7 +87,7 @@ container_scanning:
variables:
DOCKER_DRIVER: overlay2
## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables
+ ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
allow_failure: true
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
index 8873a1596f7..6499413baf0 100644
--- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -104,7 +104,7 @@ to ensure our deployments only happen when we push to the master branch.
Now, since the steps defined in `.gitlab-ci.yml` require credentials to login
to CF, you'll need to add your CF credentials as [environment
-variables](../../variables/README.md#predefined-variables-environment-variables)
+variables](../../variables/README.md#predefined-environment-variables)
on GitLab CI/CD. To set the environment variables, navigate to your project's
**Settings > CI/CD** and expand **Variables**. Name the variables
`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values.
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index b59271e400f..61bf68fa0e8 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -47,7 +47,7 @@ This project has three jobs:
## Store API keys
-You'll need to create two variables in **Settings > CI/CD > Variables** in your GitLab project:
+You'll need to create two variables in **Settings > CI/CD > Environment variables** in your GitLab project:
- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index 33a353f17f5..46e6efccaf8 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -43,7 +43,7 @@ This project has three jobs:
## Store API keys
-You'll need to create two variables in your project's **Settings > CI/CD > Variables**:
+You'll need to create two variables in your project's **Settings > CI/CD > Environment variables**:
- `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app.
- `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 0cf9daed22f..2a4160f62b0 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -1,4 +1,4 @@
-# Interactive Web Terminals **[CORE ONLY]**
+# Interactive Web Terminals
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3.
@@ -9,10 +9,11 @@ is deployed, some [security precautions](../../administration/integration/termin
taken to protect the users.
NOTE: **Note:**
-GitLab.com does not support interactive web terminal at the moment – neither
-using shared GitLab.com runners nor your own runners. Please follow
-[this issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for
-progress.
+[Shared runners on GitLab.com](../quick_start/README.md#shared-runners) do not
+provide an interactive web terminal. Follow [this
+issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for progress on
+adding support. For groups and projects hosted on GitLab.com, interactive web
+terminals are available when using your own group or project runner.
## Configuration
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index c9a60feb73f..61037360326 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -224,5 +224,5 @@ removed with one of the future versions of GitLab. You are advised to
[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
[ee]: https://about.gitlab.com/pricing/
[variables]: ../variables/README.md
-[predef]: ../variables/README.md#predefined-variables-environment-variables
+[predef]: ../variables/README.md#predefined-environment-variables
[registry]: ../../user/project/container_registry.md
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 25d189afb24..97e133a2e2f 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -1,26 +1,36 @@
+---
+table_display_block: true
+---
+
# GitLab CI/CD Variables
-When receiving a job from GitLab CI, the [Runner] prepares the build environment.
-It starts by setting a list of **predefined variables** (environment variables)
-and a list of **user-defined variables**.
+When receiving a job from GitLab CI, the [Runner](https://docs.gitlab.com/runner/) prepares the build environment.
+It starts by setting a list of:
+
+- [Predefined environment variables](#predefined-environment-variables).
+- Other variables.
## Priority of variables
-The variables can be overwritten and they take precedence over each other in
-this order:
+Variables of different types can take precedence over other variables, depending on where they are defined.
+
+The order of precedence for variables is (from highest to lowest):
-1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all)
-1. Project-level [variables](#variables) or [protected variables](#protected-variables)
-1. Group-level [variables](#variables) or [protected variables](#protected-variables)
-1. YAML-defined [job-level variables](../yaml/README.md#variables)
-1. YAML-defined [global variables](../yaml/README.md#variables)
-1. [Deployment variables](#deployment-variables)
-1. [Predefined variables](#predefined-variables-environment-variables) (are the
- lowest in the chain)
+1. [Trigger variables](../triggers/README.md#pass-job-variables-to-a-trigger) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables).
+1. Project-level [variables](#variables) or [protected variables](#protected-variables).
+1. Group-level [variables](#variables) or [protected variables](#protected-variables).
+1. YAML-defined [job-level variables](../yaml/README.md#variables).
+1. YAML-defined [global variables](../yaml/README.md#variables).
+1. [Deployment variables](#deployment-variables).
+1. [Predefined environment variables](#predefined-environment-variables).
-For example, if you define `API_TOKEN=secure` as a project variable and
-`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value
-`secure` as the project variables are higher in the chain.
+For example, you define:
+
+- `API_TOKEN=secure` as a project variable.
+- `API_TOKEN=yaml` in your `.gitlab-ci.yml`.
+
+`API_TOKEN` will take the value `secure` as the project variables take precedence over those defined
+in `.gitlab-ci.yml`.
## Unsupported variables
@@ -28,10 +38,10 @@ There are cases where some variables cannot be used in the context of a
`.gitlab-ci.yml` definition (for example under `script`). Read more
about which variables are [not supported](where_variables_can_be_used.md).
-## Predefined variables (Environment variables)
+## Predefined environment variables
Some of the predefined environment variables are available only if a minimum
-version of [GitLab Runner][runner] is used. Consult the table below to find the
+version of [GitLab Runner](https://docs.gitlab.com/runner/) is used. Consult the table below to find the
version of Runner required.
NOTE: **Note:**
@@ -55,12 +65,12 @@ future GitLab releases.**
| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
-| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
-| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
-| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
-| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
+| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmenturl) is set. |
+| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
+| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. |
| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
@@ -81,6 +91,8 @@ future GitLab releases.**
| **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
| **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
| **CI_API_V4_URL** | 11.7 | all | The GitLab API v4 root URL |
+| **CI_PAGES_DOMAIN** | 11.8 | all | The configured domain that hosts GitLab Pages. |
+| **CI_PAGES_URL** | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
| **CI_PIPELINE_ID** | 8.10 | all | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project |
| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
@@ -92,7 +104,7 @@ future GitLab releases.**
| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
+| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP(S) address to access project |
| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
@@ -154,7 +166,7 @@ This feature requires GitLab Runner 0.5.0 or higher and GitLab 7.14 or higher.
GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the
build environment. The variables are hence saved in the repository, and they
-are meant to store non-sensitive project configuration, e.g., `RAILS_ENV` or
+are meant to store non-sensitive project configuration. For example, `RAILS_ENV` or
`DATABASE_URL`.
For example, if you set the variable below globally (not inside a job), it will
@@ -202,16 +214,18 @@ GitLab CI allows you to define per-project or per-group variables
that are set in the pipeline environment. The variables are stored out of
the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
making them available during a pipeline run. It's the recommended method to
-use for storing things like passwords, SSH keys and credentials.
+use for storing things like passwords, SSH keys, and credentials.
+
+Project-level variables can be added by:
-Project-level variables can be added by going to your project's
-**Settings > CI/CD**, then finding the section called **Variables**.
+1. Navigating to your project's **Settings > CI/CD** page.
+1. Inputing variable keys and values in the **Environment variables** section.
-Likewise, group-level variables can be added by going to your group's
-**Settings > CI/CD**, then finding the section called **Variables**.
-Any variables of [subgroups] will be inherited recursively.
+Group-level variables can be added by:
-![Variables](img/variables.png)
+1. Navigating to your group's **Settings > CI/CD** page.
+1. Inputing variable keys and values in the **Environment variables** section. Any variables of
+ [subgroups](../../user/group/subgroups/index.md) will be inherited recursively.
Once you set them, they will be available for all subsequent pipelines. You can also
[protect your variables](#protected-variables).
@@ -391,6 +405,10 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_SERVER_VERSION=8.14.3-ee
++ export CI_SERVER_REVISION=82823
++ CI_SERVER_REVISION=82823
+++ export CI_PAGES_DOMAIN=gitlab.io
+++ CI_PAGES_DOMAIN=gitlab.io
+++ export CI_PAGES_URL=https://gitlab-examples.gitlab.io/ci-debug-trace
+++ CI_PAGES_URL=https://gitlab-examples.gitlab.io/ci-debug-trace
++ export CI_PROJECT_ID=17893
++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace
@@ -494,6 +512,8 @@ export CI_JOB_TRIGGERED="true"
export CI_JOB_TOKEN="abcde-1234ABCD5678ef"
export CI_PIPELINE_ID="1000"
export CI_PIPELINE_IID="10"
+export CI_PAGES_DOMAIN="gitlab.io"
+export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-ce"
export CI_PROJECT_ID="34"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_NAME="gitlab-ce"
@@ -609,11 +629,8 @@ Below you can find supported syntax reference:
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
-[runner]: https://docs.gitlab.com/runner/
[shellexecutors]: https://docs.gitlab.com/runner/executors/
[triggered]: ../triggers/README.md
-[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
-[subgroups]: ../../user/group/subgroups/index.md
[builds-policies]: ../yaml/README.md#only-and-except-complex
[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
[registry]: ../../user/project/container_registry.md
diff --git a/doc/ci/variables/img/variables.png b/doc/ci/variables/img/variables.png
deleted file mode 100644
index 0795f7c888f..00000000000
--- a/doc/ci/variables/img/variables.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index fb69d888b94..4c39b14b1d0 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1520,7 +1520,7 @@ parallel. This value has to be greater than or equal to two (2) and less than or
This creates N instances of the same job that run in parallel. They're named
sequentially from `job_name 1/N` to `job_name N/N`.
-For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-variables-environment-variables) are set.
+For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-environment-variables) are set.
A simple example:
@@ -1977,7 +1977,7 @@ The YAML-defined variables are also set to all created service containers,
thus allowing to fine tune them.
Except for the user defined variables, there are also the ones [set up by the
-Runner itself](../variables/README.md#predefined-variables-environment-variables).
+Runner itself](../variables/README.md#predefined-environment-variables).
One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called
@@ -2027,8 +2027,8 @@ variables:
```
NOTE: **Note:** `GIT_STRATEGY` is not supported for
-[Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html),
-but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847)
+[Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html),
+but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847)
for updates.
### Git submodule strategy
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c188819560e..cda66447c2c 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -95,6 +95,20 @@ yield a useful result, and ensuring content is helpful and easy to consume.
- List item 2
```
+### Tables overlapping the ToC
+
+By default, all tables have a width of 100% on docs.gitlab.com.
+In a few cases, the table will overlap the table of contents (ToC).
+For these cases, add an entry to the document's frontmatter to
+render them displaying block. This will make sure the table
+is displayed behind the ToC, scrolling horizontally:
+
+```md
+---
+table_display_block: true
+---
+```
+
## Emphasis
- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`).
@@ -222,6 +236,15 @@ For other punctuation rules, please refer to the
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
+### Unlinking emails
+
+By default, all email addresses will render in an email tag on docs.gitlab.com.
+To escape the code block and unlink email addresses, use two backticks:
+
+```md
+`` example@email.com ``
+```
+
## Navigation
To indicate the steps of navigation through the UI:
@@ -262,6 +285,16 @@ Inside the document:
- If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading.
+### Remove image shadow
+
+All images displayed on docs.gitlab.com have a box shadow by default.
+To remove the box shadow, use the image class `.image-noshadow` applied
+directly to an HTML `img` tag:
+
+```html
+<img src="path/to/image.jpg" alt="Alt text (required)" class="image-noshadow">
+```
+
## Code blocks
- Always wrap code added to a sentence in inline code blocks (``` ` ```).
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 790b1bf951b..e0985922443 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -839,6 +839,20 @@ For example there can be an
`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an
EE counterpart
`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`.
+The corresponding import statement would then look like this:
+
+```javascript
+// app/assets/javascripts/protected_branches/protected_branches_bundle.js
+import bundle from '~/protected_branches/protected_branches_bundle.js';
+
+// ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+// (only works in EE)
+import bundle from 'ee/protected_branches/protected_branches_bundle.js';
+
+// in CE: app/assets/javascripts/protected_branches/protected_branches_bundle.js
+// in EE: ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js';
+```
See the frontend guide [performance section](./fe_guide/performance.md) for
information on managing page-specific javascript within EE.
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 6323275426f..00db58a45a2 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -228,6 +228,16 @@ This makes use of [`Intl.DateTimeFormat`].
[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+- In Ruby/HAML, we have two ways of adding format to dates and times:
+
+ 1. **Through the `l` helper**, i.e. `l(active_session.created_at, format: :short)`. We have some predefined formats for
+[dates](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L54) and [times](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L261).
+ If you need to add a new format, because other parts of the code could benefit from it,
+ you'll need to add it to [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) file.
+ 2. **Through `strftime`**, i.e. `milestone.start_date.strftime('%b %-d')`. We use `strftime` in case none of the formats
+ defined on [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) matches the date/time
+ specifications we need, and if there is no need to add it as a new format because is very particular (i.e. it's only used in a single view).
+
## Best practices
### Splitting sentences
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b3ad1c5a91c..1f65e3415d1 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -6,7 +6,8 @@ Since an installation from source is a lot of work and error prone we strongly r
One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes.
On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time.
-Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory.
+
+Omnibus packages solve this by [letting the Sidekiq terminate gracefully](../administration/operations/sidekiq_memory_killer.md) if it uses too much memory.
After this termination Runit will detect Sidekiq is not running and will start it.
Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time.
@@ -15,19 +16,19 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-7-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
-If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
+If the highest number stable branch is unclear, check the [GitLab blog](https://about.gitlab.com/blog/) for installation guide links by version.
## Important Notes
This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
-This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
+This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS, we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/).
-This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation).
+This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options, see [the installation section of the README](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation).
-The following steps have been known to work. Please **use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example many people run into permission problems because they changed the location of directories or run services as the wrong user.
+The following steps have been known to work. **Use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example, many people run into permission problems because they changed the location of directories or run services as the wrong user.
-If you find a bug/error in this guide please **submit a merge request**
+If you find a bug/error in this guide, **submit a merge request**
following the
[contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
@@ -35,17 +36,17 @@ following the
The GitLab installation consists of setting up the following components:
-1. Packages / Dependencies
-1. Ruby
-1. Go
-1. Node
-1. System Users
-1. Database
-1. Redis
-1. GitLab
-1. Nginx
+1. [Packages and dependencies](#1-packages-and-dependencies).
+1. [Ruby](#2-ruby).
+1. [Go](#3-go).
+1. [Node](#4-node).
+1. [System users](#5-system-users).
+1. [Database](#6-database).
+1. [Redis](#7-redis).
+1. [GitLab](#8-gitlab).
+1. [Nginx](#9-nginx).
-## 1. Packages / Dependencies
+## 1. Packages and dependencies
`sudo` is not installed on Debian by default. Make sure your system is
up-to-date and install it.
@@ -57,7 +58,8 @@ apt-get upgrade -y
apt-get install sudo -y
```
-**Note:** During this installation some files will need to be edited manually. If you are familiar with vim set it as default editor with the commands below. If you are not familiar with vim please skip this and keep using the default editor.
+NOTE: **Note:**
+During this installation, some files will need to be edited manually. If you are familiar with vim, set it as default editor with the commands below. If you are not familiar with vim, skip this and keep using the default editor.
```sh
# Install vim and set as default editor
@@ -76,15 +78,16 @@ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdb
Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
you can [install re2 manually](https://github.com/google/re2/wiki/Install).
-If you want to use Kerberos for user authentication, then install libkrb5-dev:
+If you want to use Kerberos for user authentication, install `libkrb5-dev`:
```sh
sudo apt-get install libkrb5-dev
```
-**Note:** If you don't know what Kerberos is, you can assume you don't need it.
+NOTE: **Note:**
+If you don't know what Kerberos is, you can assume you don't need it.
-Make sure you have the right version of Git installed
+Make sure you have the right version of Git installed:
```sh
# Install Git
@@ -117,7 +120,7 @@ sudo make prefix=/usr/local install
# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git
```
-For the [Custom Favicon](../customization/favicon.md) to work, graphicsmagick
+For the [Custom Favicon](../customization/favicon.md) to work, GraphicsMagick
needs to be installed.
```sh
@@ -167,7 +170,7 @@ make
sudo make install
```
-Then install the Bundler Gem:
+Then install the Bundler gem (a version below 2.x):
```sh
sudo gem install bundler --no-document --version '< 2'
@@ -193,9 +196,14 @@ rm go1.10.3.linux-amd64.tar.gz
## 4. Node
-Since GitLab 8.17, GitLab requires the use of Node to compile javascript
-assets, and Yarn to manage javascript dependencies. The current minimum
-requirements for these are node >= v8.10.0 and yarn >= v1.10.0. In many distros
+Since GitLab 8.17, GitLab requires the use of Node to compile JavaScript
+assets, and Yarn to manage JavaScript dependencies. The current minimum
+requirements for these are:
+
+- `node` >= v8.10.0.
+- `yarn` >= v1.10.0.
+
+In many distros,
the versions provided by the official package repositories are out of date, so
we'll need to install through the following commands:
@@ -212,7 +220,7 @@ sudo apt-get install yarn
Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps.
-## 5. System Users
+## 5. System users
Create a `git` user for GitLab:
@@ -222,11 +230,10 @@ sudo adduser --disabled-login --gecos 'GitLab' git
## 6. Database
-We recommend using a PostgreSQL database. For MySQL check the
-[MySQL setup guide](database_mysql.md).
+We recommend using a PostgreSQL database. For MySQL, see the [MySQL setup guide](database_mysql.md).
-> **Note**: because we need to make use of extensions and concurrent index removal,
-you need at least PostgreSQL 9.2.
+NOTE: **Note:**
+Because we need to make use of extensions and concurrent index removal, you need at least PostgreSQL 9.2.
1. Install the database packages:
@@ -286,7 +293,7 @@ you need at least PostgreSQL 9.2.
GitLab requires at least Redis 2.8.
-If you are using Debian 8 or Ubuntu 14.04 and up, then you can simply install
+If you are using Debian 8 or Ubuntu 14.04 and up, you can simply install
Redis 2.8 with:
```sh
@@ -341,7 +348,8 @@ cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab
```
-**Note:** You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+CAUTION: **Caution:**
+You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install `master` on a production server!
### Configure It
@@ -419,9 +427,11 @@ sudo -u git -H cp config/resque.yml.example config/resque.yml
sudo -u git -H editor config/resque.yml
```
-**Important Note:** Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup.
+CAUTION: **Caution:**
+Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup.
-**Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
+NOTE: **Note:**
+If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
### Configure GitLab DB Settings
@@ -447,7 +457,13 @@ sudo -u git -H chmod o-rwx config/database.yml
### Install Gems
-**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
+NOTE: **Note:**
+As of Bundler 1.5.2, you can invoke `bundle install -jN` (where `N` is the number of your processor cores) and enjoy parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information, see this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler).
+
+Make sure you have `bundle` (run `bundle -v`):
+
+- `>= 1.5.2`, because some [issues](https://devcenter.heroku.com/changelog-items/411) were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
+- `< 2.x`.
```sh
# For PostgreSQL (note, the option says "without ... mysql")
@@ -457,7 +473,8 @@ sudo -u git -H bundle install --deployment --without development test mysql aws
sudo -u git -H bundle install --deployment --without development test postgres aws kerberos
```
-**Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above.
+NOTE: **Note:**
+If you want to use Kerberos for user authentication, omit `kerberos` in the `--without` option above.
### Install GitLab Shell
@@ -472,11 +489,14 @@ sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/red
sudo -u git -H editor /home/git/gitlab-shell/config.yml
```
-**Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
+NOTE: **Note:**
+If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
-**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
+NOTE: **Note:**
+Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in `/etc/hosts` ("127.0.0.1 hostname"). This might be necessary, for example, if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
-**Note:** GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several manners:
+NOTE: **Note:**
+GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several ways:
- Export `RUBYOPT=--disable-gems` environment variable for the processes.
- Compile Ruby with `configure --disable-rubygems` to disable RubyGems by default. Not recommended for system-wide Ruby.
@@ -498,9 +518,9 @@ You can specify a different Git repository by providing it as an extra parameter
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
```
-### Install gitlab-pages
+### Install GitLab Pages
-GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways.
+GitLab Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab Pages in `/home/git/gitlab-pages`. For additional setup steps, consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be run several different ways.
```sh
cd /home/git
@@ -550,7 +570,8 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes
# When done you see 'Administrator account created:'
```
-**Note:** You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password.
+NOTE: **Note:**
+You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one), wait to expose GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login, you'll be forced to change the default password.
```sh
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail
@@ -576,7 +597,7 @@ And if you are installing with a non-default folder or user copy and edit the de
sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab
```
-If you installed GitLab in another directory or as a user other than the default you should change these settings in `/etc/default/gitlab`. Do not edit `/etc/init.d/gitlab` as it will be changed on upgrade.
+If you installed GitLab in another directory or as a user other than the default, you should change these settings in `/etc/default/gitlab`. Do not edit `/etc/init.d/gitlab` as it will be changed on upgrade.
Make GitLab start on boot:
@@ -621,7 +642,8 @@ sudo /etc/init.d/gitlab restart
## 9. Nginx
-**Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/).
+NOTE: **Note:**
+Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, see [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/).
### Installation
@@ -638,7 +660,7 @@ sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab
sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab
```
-Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user:
+Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the `git` user:
```sh
# Change YOUR_SERVER_FQDN to the fully-qualified
@@ -685,7 +707,7 @@ To make sure you didn't miss anything run a more thorough check with:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
-If all items are green, then congratulations on successfully installing GitLab!
+If all items are green, congratulations on successfully installing GitLab!
NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit project names from the output of the check command.
@@ -727,11 +749,11 @@ To use GitLab with HTTPS:
1. Update `ssl_certificate` and `ssl_certificate_key`.
1. Review the configuration file and consider applying other security and performance enhancing features.
-Using a self-signed certificate is discouraged but if you must use it follow the normal directions then:
+Using a self-signed certificate is discouraged but if you must use it, follow the normal directions. Then:
1. Generate a self-signed SSL certificate:
- ```
+ ```sh
mkdir -p /etc/nginx/ssl/
cd /etc/nginx/ssl/
sudo openssl req -newkey rsa:2048 -x509 -nodes -days 3560 -out gitlab.crt -keyout gitlab.key
@@ -745,16 +767,16 @@ See the ["Reply by email" documentation](../administration/reply_by_email.md) fo
### LDAP Authentication
-You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file.
+You can configure LDAP authentication in `config/gitlab.yml`. Restart GitLab after editing this file.
### Using Custom Omniauth Providers
-See the [omniauth integration document](../integration/omniauth.md)
+See the [omniauth integration document](../integration/omniauth.md).
### Build your projects
-GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
-Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
+GitLab can build your projects. To enable that feature, you need GitLab Runners to do that for you.
+See the [GitLab Runner section](https://about.gitlab.com/product/continuous-integration/#gitlab-runner) to install it.
### Adding your Trusted Proxies
@@ -776,7 +798,7 @@ production:
url: redis://redis.example.tld:6379
```
-If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file.
+If you want to connect the Redis server via socket, use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file.
```
# example
@@ -808,7 +830,7 @@ You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`,
### Additional Markup Styles
-Apart from the always supported markdown style there are other rich text files that GitLab can display. But 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.
+Apart from the always supported markdown style, there are other rich text files that GitLab can display. But you might have to install a dependency to do so. See the [github-markup gem README](https://github.com/gitlabhq/markup#markups) for more information.
## Troubleshooting
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index a69db1d1a6e..68ec8c4b5c2 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -43,9 +43,13 @@ you to use.
| :--- | :---------- |
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
| **Application description** | Fill this in if you wish. |
- | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
+ | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com/users/auth`. |
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
+ NOTE: Be sure to append `/users/auth` to the end of the callback URL
+ to prevent a [OAuth2 convert
+ redirect](http://tetraph.com/covert_redirect/) vulnerability.
+
NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
see an "Invalid redirect_uri" message. For more details, see [the
Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
diff --git a/doc/integration/github.md b/doc/integration/github.md
index b8156b2b593..eca9aa16499 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -21,9 +21,13 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com`
- Application description: Fill this in if you wish.
- - Authorization callback URL: `http(s)://${YOUR_DOMAIN}`. Please make sure the port is included if your GitLab instance is not configured on default port.
+ - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth`. Please make sure the port is included if your GitLab instance is not configured on default port.
![Register OAuth App](img/github_register_app.png)
+ NOTE: Be sure to append `/users/auth` to the end of the callback URL
+ to prevent a [OAuth2 convert
+ redirect](http://tetraph.com/covert_redirect/) vulnerability.
+
1. Select **Register application**.
1. You should now see a pair of **Client ID** and **Client Secret** near the top right of the page (see screenshot).
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 1b93cdb83ac..1d656574acd 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -3,21 +3,23 @@
## Versioning
GitLab follows the [Semantic Versioning](http://semver.org/) for its releases:
-`(Major).(Minor).(Patch)` in a [pragmatic way].
-
-- **Major version**: Whenever there is something significant or any backwards
- incompatible changes are introduced to the public API.
-- **Minor version**: When new, backwards compatible functionality is introduced
- to the public API or a minor feature is introduced, or when a set of smaller
- features is rolled out.
-- **Patch number**: When backwards compatible bug fixes are introduced that fix
- incorrect behavior.
+`(Major).(Minor).(Patch)` in a [pragmatic way](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e).
For example, for GitLab version 10.5.7:
-- `10` represents major version
-- `5` represents minor version
-- `7` represents patch number
+- `10` represents the major version. The major release was 10.0.0, but often referred to as 10.0.
+- `5` represents the minor version. The minor release was 10.5.0, but often referred to as 10.5.
+- `7` represents the patch number.
+
+Any part of the version number can increment into multiple digits, for example, 13.10.11.
+
+The following table describes the version types and their release cadence:
+
+| Version type | Description | Cadence |
+|:-------------|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Major | For significant changes, or when any backward-incompatible changes are introduced to the public API. | Yearly. The next major release is GitLab 12.0 on June 22, 2019. Subsequent major releases will be scheduled for May 22 each year, by default. | |
+| Minor | For when new backward-compatible functionality is introduced to the public API, a minor feature is introduced, or when a set of smaller features is rolled out. | Monthly on the 22nd. |
+| Patch | For backward-compatible bug fixes that fix incorrect behavior. See [Patch releases](#patch-releases). | As needed. |
## Patch releases
@@ -68,7 +70,7 @@ We cannot guarantee that upgrading between major versions will be seamless. As p
We recommend that you first upgrade to the latest available minor version within
your major version. By doing this, you can address any deprecation messages
-that could possibly change behaviour in the next major release.
+that could change behavior in the next major release.
Please see the table below for some examples:
@@ -79,9 +81,5 @@ Please see the table below for some examples:
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
More information about the release procedures can be found in our
-[release-tools documentation][rel]. You may also want to read our
-[Responsible Disclosure Policy][disclosure].
-
-[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/
-[disclosure]: https://about.gitlab.com/disclosure/
-[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e
+[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
+[Responsible Disclosure Policy](https://about.gitlab.com/security/disclosure/).
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 68e50a61151..325de50cab0 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -193,7 +193,7 @@ To add a different cluster for each environment:
and Ingress.
1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the
specified Auto DevOps domains.
-1. Navigate to your project's **Settings > CI/CD > Variables** and add
+1. Navigate to your project's **Settings > CI/CD > Environment variables** and add
the `AUTO_DEVOPS_DOMAIN` variables with their respective environment
scope.
@@ -634,6 +634,11 @@ repo or by specifying a project variable:
- **Project variable** - Create a [project variable](../../ci/variables/README.md#variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart.
+### Custom Helm chart per environment **[PREMIUM]**
+
+You can specify the use of a custom Helm chart per environment by scoping the environment variable
+to the desired environment. See [Limiting environment scopes of variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-variables-premium).
+
### Customizing `.gitlab-ci.yml`
If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the
@@ -688,7 +693,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
| `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. |
| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
-| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
+| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.|
| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).|
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 6326aadcdf2..9749bd63f2b 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -83,7 +83,7 @@ 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.
@@ -216,7 +216,7 @@ deployment and clicking a square will take you to the pod's logs page.
TIP: **Tip:**
There is only one pod hosting the application at the moment, but you can add
more pods by defining the [`REPLICAS` variable](index.md#environment-variables)
-under **Settings > CI/CD > Variables**.
+under **Settings > CI/CD > Environment variables**.
### Working with branches
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0c358390046..019652b2408 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -159,6 +159,13 @@ Confidential issues can be accessed by reporters and higher permission levels,
as well as by guest users that create a confidential issue. To learn more,
read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues).
+### Releases permissions
+
+[Project Releases](project/releases/index.md) can be read by all project
+members (Reporters, Developers, Maintainers, Owners) **except Guests**.
+Releases can be created, updated, or deleted via [Releases APIs](../api/releases/index.md)
+by project Developers, Maintainers, and Owners.
+
## Group members permissions
NOTE: **Note:**
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 2f5efbe84d9..ca02e4e9e96 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -25,3 +25,10 @@ but issues and merge requests can't be imported.
If you want to retain all metadata like issues and merge requests, you can use
the [import/export feature](../settings/import_export.md).
+
+## Migrating between two self-hosted GitLab instances
+
+The best method for migrating a project from one GitLab instance to another,
+perhaps from an old server to a new server for example, is to
+[back up the project](../../../raketasks/backup_restore.md),
+then restore it on the new server.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index d6ee678443f..a4698fd172a 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -89,7 +89,7 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
-GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/index.md).
+GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md).
You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
@@ -132,7 +132,7 @@ If the "No data found" screen continues to appear, it could be due to:
[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
[prometheus-yml]:samples/prometheus.yml
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
-[ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables
+[ci-environment-slug]: ../../../ci/variables/#predefined-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md
index a79bc2bce06..f47884996d8 100644
--- a/doc/user/project/integrations/prometheus_library/index.md
+++ b/doc/user/project/integrations/prometheus_library/index.md
@@ -29,6 +29,6 @@ In order to isolate and only display relevant metrics for a given environment,
GitLab needs a method to detect which labels are associated. To do that,
GitLab uses the defined queries and fills in the environment specific variables.
Typically this involves looking for the
-[`$CI_ENVIRONMENT_SLUG`](../../../../ci/variables/README.md#predefined-variables-environment-variables),
+[`$CI_ENVIRONMENT_SLUG`](../../../../ci/variables/README.md#predefined-environment-variables),
but may also include other information such as the project's Kubernetes namespace.
Each search query is defined in the [exporter specific documentation](#exporters).
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 6b190deaa6c..7a45c87ada0 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -34,4 +34,4 @@ Prometheus needs to be deployed into the cluster and configured properly in orde
In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available.
-Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment.
+Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 2c8a590fc45..b4f5a72e148 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -76,10 +76,10 @@ You can [search and filter the results](../../search/index.md#issues-and-merge-r
![Group Issues list view](img/group_merge_requests_list_view.png)
-## Removing the source branch
+## Deleting the source branch
-When creating a merge request, select the "Remove source branch when merge
-request accepted" option and the source branch will be removed when the merge
+When creating a merge request, select the "Delete source branch when merge
+request accepted" option and the source branch will be deleted when the merge
request is merged.
This option is also visible in an existing merge request next to the merge
@@ -87,10 +87,10 @@ request button and can be selected/deselected before merging. It's only visible
to users with [Maintainer permissions](../../permissions.md) in the source project.
If the user viewing the merge request does not have the correct permissions to
-remove the source branch and the source branch is set for removal, the merge
-request widget will show the "Removes source branch" text.
+delete the source branch and the source branch is set for deletion, the merge
+request widget will show the "Deletes source branch" text.
-![Remove source branch status](img/remove_source_branch_status.png)
+![Delete source branch status](img/remove_source_branch_status.png)
## Allow collaboration on merge requests across forks
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 9a53036b4d1..d7a1a69f29d 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -238,6 +238,6 @@ test:
[triggers]: ../../ci/triggers/README.md
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
-[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables
+[jobenv]: ../../ci/variables/README.md#predefined-environment-variables
[2fa]: ../profile/account/two_factor_authentication.md
[pat]: ../profile/personal_access_tokens.md
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index a7846b1ee18..2bb6fcd9d74 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -178,7 +178,7 @@ Supposed your repository contained the following files:
```
├── index.html
├── css
-│   └── main.css
+│ └── main.css
└── js
└── main.js
```
@@ -333,7 +333,7 @@ public/
│ └ index.html.gz
│
├── css/
-│   └─┬ main.css
+│ └─┬ main.css
│ └ main.css.gz
│
└── js/
diff --git a/lib/api/api.rb b/lib/api/api.rb
index a768b78cda5..2b42e377c74 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -100,6 +100,7 @@ module API
mount ::API::CircuitBreakers
mount ::API::Commits
mount ::API::CommitStatuses
+ mount ::API::ContainerRegistry
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
diff --git a/lib/api/container_registry.rb b/lib/api/container_registry.rb
new file mode 100644
index 00000000000..e4493910196
--- /dev/null
+++ b/lib/api/container_registry.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module API
+ class ContainerRegistry < Grape::API
+ include PaginationParams
+
+ REGISTRY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(
+ tag_name: API::NO_SLASH_URL_PART_REGEX)
+
+ before { error!('404 Not Found', 404) unless Feature.enabled?(:container_registry_api, user_project, default_enabled: true) }
+ before { authorize_read_container_images! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get a project container repositories' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ success Entities::ContainerRegistry::Repository
+ end
+ params do
+ use :pagination
+ end
+ get ':id/registry/repositories' do
+ repositories = user_project.container_repositories.ordered
+
+ present paginate(repositories), with: Entities::ContainerRegistry::Repository
+ end
+
+ desc 'Delete repository' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ end
+ params do
+ requires :repository_id, type: Integer, desc: 'The ID of the repository'
+ end
+ delete ':id/registry/repositories/:repository_id', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do
+ authorize_admin_container_image!
+
+ DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id)
+
+ status :accepted
+ end
+
+ desc 'Get a list of repositories tags' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ success Entities::ContainerRegistry::Tag
+ end
+ params do
+ requires :repository_id, type: Integer, desc: 'The ID of the repository'
+ use :pagination
+ end
+ get ':id/registry/repositories/:repository_id/tags', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do
+ authorize_read_container_image!
+
+ tags = Kaminari.paginate_array(repository.tags)
+ present paginate(tags), with: Entities::ContainerRegistry::Tag
+ end
+
+ desc 'Delete repository tags (in bulk)' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ end
+ params do
+ requires :repository_id, type: Integer, desc: 'The ID of the repository'
+ requires :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all'
+ optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name'
+ optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month'
+ end
+ delete ':id/registry/repositories/:repository_id/tags', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do
+ authorize_admin_container_image!
+
+ CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id,
+ declared_params.except(:repository_id)) # rubocop: disable CodeReuse/ActiveRecord
+
+ status :accepted
+ end
+
+ desc 'Get a details about repository tag' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ success Entities::ContainerRegistry::TagDetails
+ end
+ params do
+ requires :repository_id, type: Integer, desc: 'The ID of the repository'
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
+ get ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do
+ authorize_read_container_image!
+ validate_tag!
+
+ present tag, with: Entities::ContainerRegistry::TagDetails
+ end
+
+ desc 'Delete repository tag' do
+ detail 'This feature was introduced in GitLab 11.8.'
+ end
+ params do
+ requires :repository_id, type: Integer, desc: 'The ID of the repository'
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
+ delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do
+ authorize_destroy_container_image!
+ validate_tag!
+
+ tag.delete
+
+ status :ok
+ end
+ end
+
+ helpers do
+ def authorize_read_container_images!
+ authorize! :read_container_image, user_project
+ end
+
+ def authorize_read_container_image!
+ authorize! :read_container_image, repository
+ end
+
+ def authorize_update_container_image!
+ authorize! :update_container_image, repository
+ end
+
+ def authorize_destroy_container_image!
+ authorize! :admin_container_image, repository
+ end
+
+ def authorize_admin_container_image!
+ authorize! :admin_container_image, repository
+ end
+
+ def repository
+ @repository ||= user_project.container_repositories.find(params[:repository_id])
+ end
+
+ def tag
+ @tag ||= repository.tag(params[:tag_name])
+ end
+
+ def validate_tag!
+ not_found!('Tag') unless tag.valid?
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4edec631e8d..9f1394571d8 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1223,8 +1223,11 @@ module API
end
class Trigger < Grape::Entity
+ include ::API::Helpers::Presentable
+
expose :id
- expose :token, :description
+ expose :token
+ expose :description
expose :created_at, :updated_at, :last_used
expose :owner, using: Entities::UserBasic
end
diff --git a/lib/api/entities/container_registry.rb b/lib/api/entities/container_registry.rb
new file mode 100644
index 00000000000..00833ca7480
--- /dev/null
+++ b/lib/api/entities/container_registry.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ContainerRegistry
+ class Repository < Grape::Entity
+ expose :id
+ expose :name
+ expose :path
+ expose :location
+ expose :created_at
+ end
+
+ class Tag < Grape::Entity
+ expose :name
+ expose :path
+ expose :location
+ end
+
+ class TagDetails < Tag
+ expose :revision
+ expose :short_revision
+ expose :digest
+ expose :created_at
+ expose :total_size
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/presentable.rb b/lib/api/helpers/presentable.rb
new file mode 100644
index 00000000000..973c2132efe
--- /dev/null
+++ b/lib/api/helpers/presentable.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ ##
+ # This module makes it possible to use `app/presenters` with
+ # Grape Entities. It instantiates model presenter and passes
+ # options defined in the API endpoint to the presenter itself.
+ #
+ # present object, with: Entities::Something,
+ # current_user: current_user,
+ # another_option: 'my options'
+ #
+ # Example above will make `current_user` and `another_option`
+ # values available in the subclass of `Gitlab::View::Presenter`
+ # thorough a separate method in the presenter.
+ #
+ # The model class needs to have `::Presentable` module mixed in
+ # if you want to use `API::Helpers::Presentable`.
+ #
+ module Presentable
+ extend ActiveSupport::Concern
+
+ def initialize(object, options = {})
+ super(object.present(options), options)
+ end
+ end
+ end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 1f59b27f685..ac8fe98e55e 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -76,7 +76,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
get ':id/pipelines/:pipeline_id' do
- authorize! :read_pipeline, user_project
+ authorize! :read_pipeline, pipeline
present pipeline, with: Entities::Pipeline
end
@@ -104,7 +104,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/retry' do
- authorize! :update_pipeline, user_project
+ authorize! :update_pipeline, pipeline
pipeline.retry_failed(current_user)
@@ -119,7 +119,7 @@ module API
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/cancel' do
- authorize! :update_pipeline, user_project
+ authorize! :update_pipeline, pipeline
pipeline.cancel_running
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 604f989d8b3..8fc7c7361e1 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -51,7 +51,7 @@ module API
triggers = user_project.triggers.includes(:trigger_requests)
- present paginate(triggers), with: Entities::Trigger
+ present paginate(triggers), with: Entities::Trigger, current_user: current_user
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -68,7 +68,7 @@ module API
trigger = user_project.triggers.find(params.delete(:trigger_id))
break not_found!('Trigger') unless trigger
- present trigger, with: Entities::Trigger
+ present trigger, with: Entities::Trigger, current_user: current_user
end
desc 'Create a trigger' do
@@ -85,7 +85,7 @@ module API
declared_params(include_missing: false).merge(owner: current_user))
if trigger.valid?
- present trigger, with: Entities::Trigger
+ present trigger, with: Entities::Trigger, current_user: current_user
else
render_validation_error!(trigger)
end
@@ -106,7 +106,7 @@ module API
break not_found!('Trigger') unless trigger
if trigger.update(declared_params(include_missing: false))
- present trigger, with: Entities::Trigger
+ present trigger, with: Entities::Trigger, current_user: current_user
else
render_validation_error!(trigger)
end
@@ -127,7 +127,7 @@ module API
if trigger.update(owner: current_user)
status :ok
- present trigger, with: Entities::Trigger
+ present trigger, with: Entities::Trigger, current_user: current_user
else
render_validation_error!(trigger)
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index deda4b1872e..f3061bad4ff 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -8,6 +8,10 @@ module Banzai
#
# Based on HTML::Pipeline::AutolinkFilter
#
+ # Note that our CommonMark parser, `commonmarker` (using the autolink extension)
+ # handles standard autolinking, like http/https. We detect additional
+ # schemes (smb, rdar, etc).
+ #
# Context options:
# :autolink - Boolean, skips all processing done by this filter when false
# :link_attr - Hash of attributes for the generated links
@@ -107,10 +111,13 @@ module Banzai
end
end
- # match has come from node.to_html above, so we know it's encoded
- # correctly.
+ # Since this came from a Text node, make sure the new href is encoded.
+ # `commonmarker` percent encodes the domains of links it handles, so
+ # do the same (instead of using `normalized_encode`).
+ href_safe = Addressable::URI.encode(match).html_safe
+
html_safe_match = match.html_safe
- options = link_options.merge(href: html_safe_match)
+ options = link_options.merge(href: href_safe)
content_tag(:a, html_safe_match, options) + dropped
end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index c87948a30bf..fa1690f73ad 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/emoji.js
module Banzai
module Filter
# HTML filter that replaces :emoji: and unicode with images.
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 4f60b6f84c6..61ee3eac216 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -4,17 +4,29 @@ module Banzai
module Filter
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
- SCHEMES = ['http', 'https', nil].freeze
+ SCHEMES = ['http', 'https', nil].freeze
+ RTLO = "\u202E".freeze
+ ENCODED_RTLO = '%E2%80%AE'.freeze
def call
links.each do |node|
- uri = uri(node['href'].to_s)
-
- node.set_attribute('href', uri.to_s) if uri
+ # URI.parse does stricter checking on the url than Addressable,
+ # such as on `mailto:` links. Since we've been using it, do an
+ # initial parse for validity and then use Addressable
+ # for IDN support, etc
+ uri = uri_strict(node['href'].to_s)
+ if uri
+ node.set_attribute('href', uri.to_s)
+ addressable_uri = addressable_uri(node['href'])
+ else
+ addressable_uri = nil
+ end
- if SCHEMES.include?(uri&.scheme) && !internal_url?(uri)
- node.set_attribute('rel', 'nofollow noreferrer noopener')
- node.set_attribute('target', '_blank')
+ unless internal_url?(addressable_uri)
+ punycode_autolink_node!(addressable_uri, node)
+ sanitize_link_text!(node)
+ add_malicious_tooltip!(addressable_uri, node)
+ add_nofollow!(addressable_uri, node)
end
end
@@ -23,12 +35,18 @@ module Banzai
private
- def uri(href)
+ def uri_strict(href)
URI.parse(href)
rescue URI::Error
nil
end
+ def addressable_uri(href)
+ Addressable::URI.parse(href)
+ rescue Addressable::URI::InvalidURIError
+ nil
+ end
+
def links
query = 'descendant-or-self::a[@href and not(@href = "")]'
doc.xpath(query)
@@ -45,6 +63,57 @@ module Banzai
def internal_url
@internal_url ||= URI.parse(Gitlab.config.gitlab.url)
end
+
+ # Only replace an autolink with an IDN with it's punycode
+ # version if we need emailable links. Otherwise let it
+ # be shown normally and the tooltips will show the
+ # punycode version.
+ def punycode_autolink_node!(uri, node)
+ return unless uri
+ return unless context[:emailable_links]
+
+ unencoded_uri_str = Addressable::URI.unencode(node['href'])
+
+ if unencoded_uri_str == node.content && idn?(uri)
+ node.content = uri.normalize
+ end
+ end
+
+ # escape any right-to-left (RTLO) characters in link text
+ def sanitize_link_text!(node)
+ node.inner_html = node.inner_html.gsub(RTLO, ENCODED_RTLO)
+ end
+
+ # If the domain is an international domain name (IDN),
+ # let's expose with a tooltip in case it's intended
+ # to be malicious. This is particularly useful for links
+ # where the link text is not the same as the actual link.
+ # We will continue to show the unicode version of the domain
+ # in autolinked link text, which could contain emojis, etc.
+ #
+ # Also show the tooltip if the url contains the RTLO character,
+ # as this is an indicator of a malicious link
+ def add_malicious_tooltip!(uri, node)
+ if idn?(uri) || has_encoded_rtlo?(uri)
+ node.add_class('has-tooltip')
+ node.set_attribute('title', uri.normalize)
+ end
+ end
+
+ def add_nofollow!(uri, node)
+ if SCHEMES.include?(uri&.scheme)
+ node.set_attribute('rel', 'nofollow noreferrer noopener')
+ node.set_attribute('target', '_blank')
+ end
+ end
+
+ def idn?(uri)
+ uri&.normalized_host&.start_with?('xn--')
+ end
+
+ def has_encoded_rtlo?(uri)
+ uri&.to_s&.include?(ENCODED_RTLO)
+ end
end
end
end
diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb
index afaee70f351..d8b9eb29cf5 100644
--- a/lib/banzai/filter/image_lazy_load_filter.rb
+++ b/lib/banzai/filter/image_lazy_load_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
module Banzai
module Filter
# HTML filter that moves the value of image `src` attributes to `data-src`
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index 884a94fb761..01237303c27 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
module Banzai
module Filter
# HTML filter that wraps links around inline images.
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
index e9ddc6e0e3d..5a1c0bee32d 100644
--- a/lib/banzai/filter/inline_diff_filter.rb
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
module Banzai
module Filter
class InlineDiffFilter < HTML::Pipeline::Filter
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index e52c0d15b31..d3af776db05 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -32,8 +32,13 @@ module Banzai
:DEFAULT # default rendering system. Nothing special.
].freeze
- def initialize
- @renderer = Banzai::Renderer::CommonMark::HTML.new(options: RENDER_OPTIONS)
+ RENDER_OPTIONS_SOURCEPOS = RENDER_OPTIONS + [
+ :SOURCEPOS # enable embedding of source position information
+ ].freeze
+
+ def initialize(context)
+ @context = context
+ @renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options)
end
def render(text)
@@ -41,6 +46,12 @@ module Banzai
@renderer.render(doc)
end
+
+ private
+
+ def render_options
+ @context[:no_sourcepos] ? RENDER_OPTIONS : RENDER_OPTIONS_SOURCEPOS
+ end
end
end
end
diff --git a/lib/banzai/filter/markdown_engines/redcarpet.rb b/lib/banzai/filter/markdown_engines/redcarpet.rb
index ec150d041ff..5b3f75096b1 100644
--- a/lib/banzai/filter/markdown_engines/redcarpet.rb
+++ b/lib/banzai/filter/markdown_engines/redcarpet.rb
@@ -20,7 +20,7 @@ module Banzai
tables: true
}.freeze
- def initialize
+ def initialize(context = nil)
html_renderer = Banzai::Renderer::Redcarpet::HTML.new
@renderer = ::Redcarpet::Markdown.new(html_renderer, OPTIONS)
end
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index cdf758472c1..242e39f5495 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -6,7 +6,7 @@ module Banzai
def initialize(text, context = nil, result = nil)
super(text, context, result)
- @renderer = renderer(context[:markdown_engine]).new
+ @renderer = renderer(context[:markdown_engine]).new(context)
@text = @text.delete("\r")
end
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 9d1bc3cf60c..8dd5a8979c8 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -2,6 +2,9 @@
require 'uri'
+# Generated HTML is transformed back to GFM by:
+# - app/assets/javascripts/behaviors/markdown/marks/math.js
+# - app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
module Filter
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb
index 7c8b165a330..f0adb83af8a 100644
--- a/lib/banzai/filter/mermaid_filter.rb
+++ b/lib/banzai/filter/mermaid_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
module Filter
class MermaidFilter < HTML::Pipeline::Filter
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index e5164e7f72a..42f9b3a689c 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
module Banzai
module Filter
# Base class for GitLab Flavored Markdown reference filters.
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index edc053638a8..a4a06eae7b7 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -41,6 +41,9 @@ module Banzai
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
+ # Allow the 'data-sourcepos' from CommonMark on all elements
+ whitelist[:attributes][:all].push('data-sourcepos')
+
# Disallow `name` attribute globally, allow on `a`
whitelist[:attributes][:all].delete('name')
whitelist[:attributes]['a'].push('name')
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index c6a3a763c23..00dbf2d3130 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -73,7 +73,8 @@ module Banzai
html = Banzai::Filter::MarkdownFilter.call(transform_markdown(match), context)
# link is wrapped in a <p>, so strip that off
- html.sub('<p>', '').chomp('</p>')
+ p_node = Nokogiri::HTML.fragment(html).at_css('p')
+ p_node ? p_node.children.to_html : html
end
def spaced_link_filter(text)
diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb
index 307ea449140..9950db373d8 100644
--- a/lib/banzai/filter/suggestion_filter.rb
+++ b/lib/banzai/filter/suggestion_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
module Filter
class SuggestionFilter < HTML::Pipeline::Filter
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 18e5e9185de..bcf77861f10 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -3,6 +3,7 @@
require 'rouge/plugins/common_mark'
require 'rouge/plugins/redcarpet'
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
module Banzai
module Filter
# HTML Filter to highlight fenced code blocks
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index c6d1e028eaa..f2ae17b44fa 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
module Banzai
module Filter
# HTML filter that adds an anchor child element to all Headers in a
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index ef35a49edcb..c6b402575cb 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -2,6 +2,10 @@
require 'task_list/filter'
+# Generated HTML is transformed back to GFM by:
+# - app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
+# - app/assets/javascripts/behaviors/markdown/nodes/task_list.js
+# - app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
module Banzai
module Filter
class TaskListFilter < TaskList::Filter
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index 0fb59c914c3..0fff104cf91 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/video.js
module Banzai
module Filter
# Find every image that isn't already wrapped in an `a` tag, and that has
diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb
index 13a342351b6..c632910585d 100644
--- a/lib/banzai/pipeline/atom_pipeline.rb
+++ b/lib/banzai/pipeline/atom_pipeline.rb
@@ -6,7 +6,8 @@ module Banzai
def self.transform_context(context)
super(context).merge(
only_path: false,
- xhtml: true
+ xhtml: true,
+ no_sourcepos: true
)
end
end
diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb
index a3d63e0aaf5..580b5b72474 100644
--- a/lib/banzai/pipeline/broadcast_message_pipeline.rb
+++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb
@@ -14,6 +14,12 @@ module Banzai
Filter::ExternalLinkFilter
]
end
+
+ def self.transform_context(context)
+ super(context).merge(
+ no_sourcepos: true
+ )
+ end
end
end
end
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
index 2c08581ce0d..13e6a990407 100644
--- a/lib/banzai/pipeline/email_pipeline.rb
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -11,7 +11,9 @@ module Banzai
def self.transform_context(context)
super(context).merge(
- only_path: false
+ only_path: false,
+ emailable_links: true,
+ no_sourcepos: true
)
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index d860dad0b6c..30cafd11834 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -3,11 +3,11 @@
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
- # These filters convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
- # consequently convert that same HTML to GFM to be copied to the clipboard.
- # Every filter that generates HTML from GFM should have a handler in
- # app/assets/javascripts/behaviors/markdown/copy_as_gfm.js, in reverse order.
+ # These filters transform GitLab Flavored Markdown (GFM) to HTML.
+ # The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js
+ # consequently transform that same HTML to GFM to be copied to the clipboard.
+ # Every filter that generates HTML from GFM should have a node or mark in
+ # app/assets/javascripts/behaviors/markdown/editor_extensions.js.
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 61ff7b0bcce..72374207a8f 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -27,6 +27,12 @@ module Banzai
Filter::CommitReferenceFilter
]
end
+
+ def self.transform_context(context)
+ super(context).merge(
+ no_sourcepos: true
+ )
+ end
end
end
end
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 8633e764f90..ef41dc560c9 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -2,6 +2,8 @@
module ContainerRegistry
class Tag
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :repository, :name
delegate :registry, :client, to: :repository
@@ -15,6 +17,10 @@ module ContainerRegistry
manifest.present?
end
+ def latest?
+ name == "latest"
+ end
+
def v1?
manifest && manifest['schemaVersion'] == 1
end
@@ -24,7 +30,9 @@ module ContainerRegistry
end
def manifest
- @manifest ||= client.repository_manifest(repository.path, name)
+ strong_memoize(:manifest) do
+ client.repository_manifest(repository.path, name)
+ end
end
def path
@@ -42,36 +50,44 @@ module ContainerRegistry
end
def digest
- @digest ||= client.repository_tag_digest(repository.path, name)
+ strong_memoize(:digest) do
+ client.repository_tag_digest(repository.path, name)
+ end
end
def config_blob
- return @config_blob if defined?(@config_blob)
return unless manifest && manifest['config']
- @config_blob = repository.blob(manifest['config'])
+ strong_memoize(:config_blob) do
+ repository.blob(manifest['config'])
+ end
end
def config
- return unless config_blob
+ return unless config_blob&.data
- @config ||= ContainerRegistry::Config.new(self, config_blob) if config_blob.data
+ strong_memoize(:config) do
+ ContainerRegistry::Config.new(self, config_blob)
+ end
end
def created_at
return unless config
- @created_at ||= DateTime.rfc3339(config['created'])
+ strong_memoize(:created_at) do
+ DateTime.rfc3339(config['created'])
+ end
end
def layers
- return @layers if defined?(@layers)
return unless manifest
- layers = manifest['layers'] || manifest['fsLayers']
+ strong_memoize(:layers) do
+ layers = manifest['layers'] || manifest['fsLayers']
- @layers = layers.map do |layer|
- repository.blob(layer)
+ layers.map do |layer|
+ repository.blob(layer)
+ end
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index b91394f7f58..e073450283b 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -7,6 +7,14 @@ module Gitlab
Pathname.new(File.expand_path('..', __dir__))
end
+ def self.version_info
+ Gitlab::VersionInfo.parse(Gitlab::VERSION)
+ end
+
+ def self.pre_release?
+ VERSION.include?('pre')
+ end
+
def self.config
Settings
end
@@ -27,52 +35,12 @@ module Gitlab
end
end
- def self.version_info
- Gitlab::VersionInfo.parse(Gitlab::VERSION)
- end
-
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
- def self.pre_release?
- VERSION.include?('pre')
- end
-
- def self.final_release?
- !VERSION.include?('rc') && !pre_release?
- end
-
- def self.minor_release
- "#{version_info.major}.#{version_info.minor}"
- end
-
- def self.prev_minor_release
- "#{version_info.major}.#{version_info.minor - 1}"
- end
-
- def self.prev_major_release
- "#{version_info.major.to_i - 1}"
- end
-
- def self.new_major_release?
- version_info.minor.to_i.zero?
- end
-
- def self.previous_release
- if version_info.minor_version?
- if version_info.patch_version?
- minor_release
- else
- prev_minor_release
- end
- else
- prev_major_release
- end
- end
-
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 974b5ad6877..4dcb3869d4f 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -31,7 +31,7 @@ module Gitlab
end
class Converter
- def on_0(_) reset() end
+ def on_0(_) reset end
def on_1(_) enable(STYLE_SWITCHES[:bold]) end
@@ -177,7 +177,7 @@ module Gitlab
end
end
- close_open_tags()
+ close_open_tags
OpenStruct.new(
html: @out.force_encoding(Encoding.default_external),
@@ -194,7 +194,7 @@ module Gitlab
action = scanner[1]
timestamp = scanner[2]
section = scanner[3]
- line = scanner.matched()[0...-5] # strips \r\033[0K
+ line = scanner.matched[0...-5] # strips \r\033[0K
@out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
end
@@ -209,10 +209,10 @@ module Gitlab
# sequence gets stripped (including stuff like "delete last line")
return unless indicator == '[' && terminator == 'm'
- close_open_tags()
+ close_open_tags
- if commands.empty?()
- reset()
+ if commands.empty?
+ reset
return
end
@@ -222,7 +222,7 @@ module Gitlab
end
def evaluate_command_stack(stack)
- return unless command = stack.shift()
+ return unless command = stack.shift
if self.respond_to?("on_#{command}", true)
self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
@@ -333,8 +333,8 @@ module Gitlab
return unless command_stack.length >= 2
return unless command_stack[0] == "5"
- command_stack.shift() # ignore the "5" command
- color_index = command_stack.shift().to_i
+ command_stack.shift # ignore the "5" command
+ color_index = command_stack.shift.to_i
return unless color_index >= 0
return unless color_index <= 255
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 1d8904f7b29..290c9591b98 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -67,7 +67,7 @@ module Gitlab
entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.',
- default: { refs: %w[branches tags] }
+ default: Entry::Policy::DEFAULT_ONLY
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 82b72e40404..9845c4af655 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -28,11 +28,15 @@ module Gitlab
name.to_s.start_with?('.')
end
+ def node_type(name)
+ hidden?(name) ? Entry::Hidden : Entry::Job
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def compose!(deps = nil)
super do
@config.each do |name, config|
- node = hidden?(name) ? Entry::Hidden : Entry::Job
+ node = node_type(name)
factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {})
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 9c677bf6617..adc3660d950 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -11,6 +11,8 @@ module Gitlab
strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
+ DEFAULT_ONLY = { refs: %w[branches tags] }.freeze
+
class RefsPolicy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index ef738a93bfe..d8296940a04 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -38,9 +38,17 @@ module Gitlab
)
end
+ def bridge?
+ @attributes.to_h.dig(:options, :trigger).present?
+ end
+
def to_resource
strong_memoize(:resource) do
- ::Ci::Build.new(attributes)
+ if bridge?
+ ::Ci::Bridge.new(attributes)
+ else
+ ::Ci::Build.new(attributes)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 4775ff15581..9c15064756a 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -39,7 +39,13 @@ module Gitlab
def to_resource
strong_memoize(:stage) do
::Ci::Stage.new(attributes).tap do |stage|
- seeds.each { |seed| stage.builds << seed.to_resource }
+ seeds.each do |seed|
+ if seed.bridge?
+ stage.bridges << seed.to_resource
+ else
+ stage.builds << seed.to_resource
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
index c6cb620f7a0..4746195c618 100644
--- a/lib/gitlab/ci/status/bridge/common.rb
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -18,7 +18,6 @@ module Gitlab
end
def details_path
- raise NotImplementedError
end
end
end
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
index 4169f5b3210..cd772819293 100644
--- a/lib/gitlab/ci/status/external/common.rb
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -6,7 +6,7 @@ module Gitlab
module External
module Common
def label
- subject.description
+ subject.description.presence || super
end
def has_details?
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 47e3e8cd271..75a5bf142d2 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -116,7 +116,7 @@ code_quality:
license_management:
stage: test
- image:
+ image:
name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
entrypoint: [""]
allow_failure: true
@@ -612,7 +612,7 @@ rollout 100%:
export APPLICATION_SECRET_NAME=$(application_secret_name "$track")
env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables
-
+
kubectl create secret \
-n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \
--from-env-file k8s_prefixed_variables -o yaml --dry-run |
@@ -689,6 +689,7 @@ rollout 100%:
--set application.database_url="$DATABASE_URL" \
--set application.secretName="$APPLICATION_SECRET_NAME" \
--set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
+ --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \
--set service.url="$CI_ENVIRONMENT_URL" \
--set service.additionalHosts="$additional_hosts" \
--set replicaCount="$replicas" \
@@ -724,6 +725,7 @@ rollout 100%:
--set application.database_url="$DATABASE_URL" \
--set application.secretName="$APPLICATION_SECRET_NAME" \
--set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
+ --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \
--set service.url="$CI_ENVIRONMENT_URL" \
--set service.additionalHosts="$additional_hosts" \
--set replicaCount="$replicas" \
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 0f23b95ba15..e61fb50a303 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -46,7 +46,7 @@ module Gitlab
stream.seek(offset, IO::SEEK_SET)
stream.write(data)
stream.truncate(offset + data.bytesize)
- stream.flush()
+ stream.flush
end
def set(data)
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 0c48a6ab3ac..07ba6f83d47 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -33,7 +33,7 @@ module Gitlab
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
- tag_list: job[:tags] || [],
+ tag_list: job[:tags],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
@@ -53,8 +53,9 @@ module Gitlab
retry: job[:retry],
parallel: job[:parallel],
instance: job[:instance],
- start_in: job[:start_in]
- }.compact }
+ start_in: job[:start_in],
+ trigger: job[:trigger]
+ }.compact }.compact
end
def stage_builds_attributes(stage)
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 862127110b9..ea08b5f7eae 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -93,7 +93,7 @@ module Gitlab
user_id: user.id,
user_name: user.name,
user_username: user.username,
- user_email: user.email,
+ user_email: user.public_email,
user_avatar: user.avatar_url(only_path: false),
project_id: project.id,
project: project.hook_attrs,
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index ba9730d2685..d8f4be8ada1 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -56,7 +56,7 @@ module Gitlab
raise ProjectNotFound unless author.can?(:read_project, project)
end
- raise UserNotAuthorizedError unless author.can?(permission, project || noteable)
+ raise UserNotAuthorizedError unless author.can?(permission, try(:noteable) || project)
end
def verify_record!(record:, invalid_exception:, record_name:)
diff --git a/lib/gitlab/error_tracking/project.rb b/lib/gitlab/error_tracking/project.rb
new file mode 100644
index 00000000000..93e81da5034
--- /dev/null
+++ b/lib/gitlab/error_tracking/project.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class Project
+ include ActiveModel::Model
+
+ ACCESSORS = [
+ :id, :name, :status, :slug, :organization_name,
+ :organization_id, :organization_slug
+ ].freeze
+
+ attr_accessor(*ACCESSORS)
+ end
+ end
+end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 08d7db49ad7..4d82acd9d87 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -93,7 +93,7 @@ module Gitlab
end
def markdown(text)
- Banzai.render(text, project: @source_parent, no_original_data: true)
+ Banzai.render(text, project: @source_parent, no_original_data: true, no_sourcepos: true)
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 85afbd85fe6..0ab53f8f706 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -133,7 +133,11 @@ module Gitlab
end
def self.address_metadata(storage)
- Base64.strict_encode64(JSON.dump({ storage => { 'address' => address(storage), 'token' => token(storage) } }))
+ Base64.strict_encode64(JSON.dump(storage => connection_data(storage)))
+ end
+
+ def self.connection_data(storage)
+ { 'address' => address(storage), 'token' => token(storage) }
end
# All Gitaly RPC call sites should use GitalyClient.call. This method
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
index da2f96b5c4b..147597289cf 100644
--- a/lib/gitlab/github_import/bulk_importing.rb
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -15,12 +15,10 @@ module Gitlab
end
# Bulk inserts the given rows into the database.
- def bulk_insert(model, rows, batch_size: 100, pre_hook: nil)
+ def bulk_insert(model, rows, batch_size: 100)
rows.each_slice(batch_size) do |slice|
- pre_hook.call(slice) if pre_hook
Gitlab::Database.bulk_insert(model.table_name, slice)
end
- rows
end
end
end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 4226eee85cc..656d46b6a7d 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -57,11 +57,7 @@ module Gitlab
updated_at: issue.updated_at
}
- insert_and_return_id(attributes, project.issues).tap do |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.
- project.issues.find(id).ensure_project_iid!
- end
+ insert_and_return_id(attributes, project.issues)
rescue ActiveRecord::InvalidForeignKey
# It's possible the project has been deleted since scheduling this
# job. In this case we'll just skip creating the issue.
diff --git a/lib/gitlab/github_import/importer/lfs_object_importer.rb b/lib/gitlab/github_import/importer/lfs_object_importer.rb
index a88c17aaf82..195383fd3e9 100644
--- a/lib/gitlab/github_import/importer/lfs_object_importer.rb
+++ b/lib/gitlab/github_import/importer/lfs_object_importer.rb
@@ -13,10 +13,12 @@ module Gitlab
@project = project
end
+ def lfs_download_object
+ LfsDownloadObject.new(oid: lfs_object.oid, size: lfs_object.size, link: lfs_object.link)
+ end
+
def execute
- Projects::LfsPointers::LfsDownloadService
- .new(project)
- .execute(lfs_object.oid, lfs_object.download_link)
+ Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object).execute
end
end
end
diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb
index 8d54b27374c..87cf2c8b598 100644
--- a/lib/gitlab/github_import/importer/milestones_importer.rb
+++ b/lib/gitlab/github_import/importer/milestones_importer.rb
@@ -19,20 +19,10 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def execute
- # We insert records in bulk, by-passing any standard model callbacks.
- # The pre_hook here makes sure we track internal ids consistently.
- # Note this has to be called before performing an insert of a batch
- # because we're outside a transaction scope here.
- bulk_insert(Milestone, build_milestones, pre_hook: method(:track_greatest_iid))
+ bulk_insert(Milestone, build_milestones)
build_milestones_cache
end
- def track_greatest_iid(slice)
- greatest_iid = slice.max { |e| e[:iid] }[:iid]
-
- InternalId.track_greatest(nil, { project: project }, :milestones, greatest_iid, ->(_) { project.milestones.maximum(:iid) })
- end
-
def build_milestones
build_database_rows(each_milestone)
end
diff --git a/lib/gitlab/github_import/representation/lfs_object.rb b/lib/gitlab/github_import/representation/lfs_object.rb
index debe0fa0baf..a4606173f49 100644
--- a/lib/gitlab/github_import/representation/lfs_object.rb
+++ b/lib/gitlab/github_import/representation/lfs_object.rb
@@ -9,11 +9,11 @@ module Gitlab
attr_reader :attributes
- expose_attribute :oid, :download_link
+ expose_attribute :oid, :link, :size
# Builds a lfs_object
def self.from_api_response(lfs_object)
- new({ oid: lfs_object[0], download_link: lfs_object[1] })
+ new({ oid: lfs_object.oid, link: lfs_object.link, size: lfs_object.size })
end
# Builds a new lfs_object using a Hash that was built from a JSON payload.
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
index 1f29cf10cad..bf463077dcc 100644
--- a/lib/gitlab/hashed_storage/migrator.rb
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -11,21 +11,21 @@ module Gitlab
# Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously
#
- # @param [Object] start first project id for the range
- # @param [Object] finish last project id for the range
- def bulk_schedule(start, finish)
- StorageMigratorWorker.perform_async(start, finish)
+ # @param [Integer] start first project id for the range
+ # @param [Integer] finish last project id for the range
+ def bulk_schedule(start:, finish:)
+ ::HashedStorage::MigratorWorker.perform_async(start, finish)
end
# Start migration of projects from specified range
#
- # Flagging a project to be migrated is a synchronous action,
+ # Flagging a project to be migrated is a synchronous action
# but the migration runs through async jobs
#
- # @param [Object] start first project id for the range
- # @param [Object] finish last project id for the range
+ # @param [Integer] start first project id for the range
+ # @param [Integer] finish last project id for the range
# rubocop: disable CodeReuse/ActiveRecord
- def bulk_migrate(start, finish)
+ def bulk_migrate(start:, finish:)
projects = build_relation(start, finish)
projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
@@ -34,9 +34,9 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # Flag a project to be migrated
+ # Flag a project to be migrated to Hashed Storage
#
- # @param [Object] project that will be migrated
+ # @param [Project] project that will be migrated
def migrate(project)
Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
@@ -45,6 +45,10 @@ module Gitlab
Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
end
+ def rollback(project)
+ # TODO: implement rollback strategy
+ end
+
private
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb
index 9215067d973..fa3ff6c3f12 100644
--- a/lib/gitlab/import/merge_request_helpers.rb
+++ b/lib/gitlab/import/merge_request_helpers.rb
@@ -24,10 +24,6 @@ module Gitlab
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.
- merge_request.ensure_target_project_iid!
-
[merge_request, false]
end
rescue ActiveRecord::InvalidForeignKey
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index a56ec65b9f1..51001750a6c 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -107,7 +107,7 @@ module Gitlab
def project_params
@project_params ||= begin
- attrs = json_params.merge(override_params)
+ attrs = json_params.merge(override_params).merge(visibility_level)
# Cleaning all imported and overridden params
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
@@ -127,6 +127,13 @@ module Gitlab
end
end
+ def visibility_level
+ level = override_params['visibility_level'] || json_params['visibility_level'] || @project.visibility_level
+ level = @project.group.visibility_level if @project.group && level > @project.group.visibility_level
+
+ { 'visibility_level' => level }
+ end
+
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index c13e6c1d83b..947caaaefee 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -8,6 +8,7 @@ module Gitlab
def initialize(project)
@project = project
@errors = []
+ @logger = Gitlab::Import::Logger.build
end
def active_export_count
@@ -23,19 +24,16 @@ module Gitlab
end
def error(error)
- error_out(error.message, caller[0].dup)
- add_error_message(error.message)
+ log_error(message: error.message, caller: caller[0].dup)
+ log_debug(backtrace: error.backtrace&.join("\n"))
+
+ Gitlab::Sentry.track_acceptable_exception(error, extra: log_base_data)
- # Debug:
- if error.backtrace
- Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}")
- else
- Rails.logger.error("No backtrace found")
- end
+ add_error_message(error.message)
end
- def add_error_message(error_message)
- @errors << error_message
+ def add_error_message(message)
+ @errors << filtered_error_message(message)
end
def after_export_in_progress?
@@ -52,8 +50,25 @@ module Gitlab
@project.disk_path
end
- def error_out(message, caller)
- Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
+ def log_error(details)
+ @logger.error(log_base_data.merge(details))
+ end
+
+ def log_debug(details)
+ @logger.debug(log_base_data.merge(details))
+ end
+
+ def log_base_data
+ {
+ importer: 'Import/Export',
+ import_jid: @project&.import_state&.import_jid,
+ project_id: @project&.id,
+ project_path: @project&.full_path
+ }
+ end
+
+ def filtered_error_message(message)
+ Projects::ImportErrorFilter.filter_message(message)
end
def after_export_lock_file
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index fe839940f74..624c2c67551 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -76,9 +76,12 @@ module Gitlab
attr_reader :api_prefix, :kubeclient_options
+ # We disable redirects through 'http_max_redirects: 0',
+ # so that KubeClient does not follow redirects and
+ # expose internal services.
def initialize(api_prefix, **kubeclient_options)
@api_prefix = api_prefix
- @kubeclient_options = kubeclient_options
+ @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0)
end
def create_or_update_cluster_role_binding(resource)
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 1359e973590..0b04340fbb5 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -147,9 +147,7 @@ module Gitlab
#
# See `Gitlab::Metrics::Transaction#add_event` for more details.
def add_event(*args)
- trans = current_transaction
-
- trans&.add_event(*args)
+ current_transaction&.add_event(*args)
end
# Returns the prefix to use for the name of a series.
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index fa68dead80b..3c888be0710 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -125,7 +125,8 @@ module Gitlab
# allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
# `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
- PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
+ PATH_START_CHAR = '[a-zA-Z0-9_\.]'.freeze
+ PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*'.freeze
NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
diff --git a/lib/gitlab/release_blog_post.rb b/lib/gitlab/release_blog_post.rb
deleted file mode 100644
index 639aee61464..00000000000
--- a/lib/gitlab/release_blog_post.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-require 'singleton'
-
-module Gitlab
- class ReleaseBlogPost
- include Singleton
-
- RELEASE_RSS_URL = 'https://about.gitlab.com/releases.xml'
-
- def blog_post_url
- @url ||= fetch_blog_post_url
- end
-
- private
-
- def fetch_blog_post_url
- installed_version = Gitlab.final_release? ? Gitlab.minor_release : Gitlab.previous_release
- response = Gitlab::HTTP.get(RELEASE_RSS_URL, verify: false)
-
- return unless response.code == 200
-
- blog_entry = find_installed_blog_entry(response, installed_version)
- blog_entry['id'] if blog_entry
- end
-
- def find_installed_blog_entry(response, installed_version)
- response['feed']['entry'].find do |entry|
- entry['release'] == installed_version || matches_previous_release_post(entry['release'], installed_version)
- end
- end
-
- def should_match_previous_release_post?
- Gitlab.new_major_release? && !Gitlab.final_release?
- end
-
- def matches_previous_release_post(rss_release_version, installed_version)
- should_match_previous_release_post? && rss_release_version[/\d+/] == installed_version
- end
- end
-end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index d24d5116167..f05592fc3a3 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -9,7 +9,7 @@ module Gitlab
#
# Example usage:
#
- # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects)
+ # union = Gitlab::SQL::Union.new([user.personal_projects, user.projects])
# sql = union.to_sql
#
# Project.where("id IN (#{sql})")
diff --git a/lib/gitlab/tracing/common.rb b/lib/gitlab/tracing/common.rb
index 5e2b12e3f90..3a08ede8138 100644
--- a/lib/gitlab/tracing/common.rb
+++ b/lib/gitlab/tracing/common.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'opentracing'
+
module Gitlab
module Tracing
module Common
@@ -32,6 +34,14 @@ module Gitlab
end
end
+ def postnotify_span(operation_name, start_time, end_time, tags: nil, child_of: nil, exception: nil)
+ span = OpenTracing.start_span(operation_name, start_time: start_time, tags: tags, child_of: child_of)
+
+ log_exception_on_span(span, exception) if exception
+
+ span.finish(end_time: end_time)
+ end
+
def log_exception_on_span(span, exception)
span.set_tag('error', true)
span.log_kv(kv_tags_for_exception(exception))
@@ -44,7 +54,7 @@ module Gitlab
'event': 'error',
'error.kind': exception.class.to_s,
'message': Gitlab::UrlSanitizer.sanitize(exception.message),
- 'stack': exception.backtrace.join("\n")
+ 'stack': exception.backtrace&.join("\n")
}
else
{
diff --git a/lib/gitlab/tracing/rails/action_view_subscriber.rb b/lib/gitlab/tracing/rails/action_view_subscriber.rb
new file mode 100644
index 00000000000..88816e1fb32
--- /dev/null
+++ b/lib/gitlab/tracing/rails/action_view_subscriber.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Tracing
+ module Rails
+ class ActionViewSubscriber
+ include RailsCommon
+
+ COMPONENT_TAG = 'ActionView'
+ RENDER_TEMPLATE_NOTIFICATION_TOPIC = 'render_template.action_view'
+ RENDER_COLLECTION_NOTIFICATION_TOPIC = 'render_collection.action_view'
+ RENDER_PARTIAL_NOTIFICATION_TOPIC = 'render_partial.action_view'
+
+ # Instruments Rails ActionView events for opentracing.
+ # Returns a lambda, which, when called will unsubscribe from the notifications
+ def self.instrument
+ subscriber = new
+
+ subscriptions = [
+ ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
+ subscriber.notify_render_template(start, finish, payload)
+ end,
+ ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
+ subscriber.notify_render_collection(start, finish, payload)
+ end,
+ ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
+ subscriber.notify_render_partial(start, finish, payload)
+ end
+ ]
+
+ create_unsubscriber subscriptions
+ end
+
+ # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
+ def notify_render_template(start, finish, payload)
+ generate_span_for_notification("render_template", start, finish, payload, tags_for_render_template(payload))
+ end
+
+ def notify_render_collection(start, finish, payload)
+ generate_span_for_notification("render_collection", start, finish, payload, tags_for_render_collection(payload))
+ end
+
+ def notify_render_partial(start, finish, payload)
+ generate_span_for_notification("render_partial", start, finish, payload, tags_for_render_partial(payload))
+ end
+
+ private
+
+ def tags_for_render_template(payload)
+ {
+ 'component' => COMPONENT_TAG,
+ 'template.id' => payload[:identifier],
+ 'template.layout' => payload[:layout]
+ }
+ end
+
+ def tags_for_render_collection(payload)
+ {
+ 'component' => COMPONENT_TAG,
+ 'template.id' => payload[:identifier],
+ 'template.count' => payload[:count] || 0,
+ 'template.cache.hits' => payload[:cache_hits] || 0
+ }
+ end
+
+ def tags_for_render_partial(payload)
+ {
+ 'component' => COMPONENT_TAG,
+ 'template.id' => payload[:identifier]
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tracing/rails/active_record_subscriber.rb b/lib/gitlab/tracing/rails/active_record_subscriber.rb
new file mode 100644
index 00000000000..32f5658e57e
--- /dev/null
+++ b/lib/gitlab/tracing/rails/active_record_subscriber.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Tracing
+ module Rails
+ class ActiveRecordSubscriber
+ include RailsCommon
+
+ ACTIVE_RECORD_NOTIFICATION_TOPIC = 'sql.active_record'
+ OPERATION_NAME_PREFIX = 'active_record:'
+ DEFAULT_OPERATION_NAME = 'sqlquery'
+
+ # Instruments Rails ActiveRecord events for opentracing.
+ # Returns a lambda, which, when called will unsubscribe from the notifications
+ def self.instrument
+ subscriber = new
+
+ subscription = ActiveSupport::Notifications.subscribe(ACTIVE_RECORD_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
+ subscriber.notify(start, finish, payload)
+ end
+
+ create_unsubscriber [subscription]
+ end
+
+ # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
+ def notify(start, finish, payload)
+ generate_span_for_notification(notification_name(payload), start, finish, payload, tags_for_notification(payload))
+ end
+
+ private
+
+ def notification_name(payload)
+ OPERATION_NAME_PREFIX + (payload[:name].presence || DEFAULT_OPERATION_NAME)
+ end
+
+ def tags_for_notification(payload)
+ {
+ 'component' => 'ActiveRecord',
+ 'span.kind' => 'client',
+ 'db.type' => 'sql',
+ 'db.connection_id' => payload[:connection_id],
+ 'db.cached' => payload[:cached] || false,
+ 'db.statement' => payload[:sql]
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tracing/rails/rails_common.rb b/lib/gitlab/tracing/rails/rails_common.rb
new file mode 100644
index 00000000000..88e914f62f8
--- /dev/null
+++ b/lib/gitlab/tracing/rails/rails_common.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Tracing
+ module Rails
+ module RailsCommon
+ extend ActiveSupport::Concern
+ include Gitlab::Tracing::Common
+
+ class_methods do
+ def create_unsubscriber(subscriptions)
+ -> { subscriptions.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) } }
+ end
+ end
+
+ def generate_span_for_notification(operation_name, start, finish, payload, tags)
+ exception = payload[:exception]
+
+ postnotify_span(operation_name, start, finish, tags: tags, exception: exception)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 083c620267a..6bfcf83f388 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -81,6 +81,7 @@ module Gitlab
pages_domains: count(PagesDomain),
projects: count(Project),
projects_imported_from_github: count(Project.where(import_type: 'github')),
+ projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
protected_branches: count(ProtectedBranch),
releases: count(Release),
remote_mirrors: count(RemoteMirror),
diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb
index 142ead12c08..aa6d5310161 100644
--- a/lib/gitlab/version_info.rb
+++ b/lib/gitlab/version_info.rb
@@ -20,14 +20,6 @@ module Gitlab
@patch = patch
end
- def minor_version?
- minor.to_i > 0
- end
-
- def patch_version?
- patch.to_i > 0
- end
-
def <=>(other)
return unless other.is_a? VersionInfo
return unless valid? && other.valid?
diff --git a/lib/safe_zip/entry.rb b/lib/safe_zip/entry.rb
new file mode 100644
index 00000000000..664e2f52f91
--- /dev/null
+++ b/lib/safe_zip/entry.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+module SafeZip
+ class Entry
+ attr_reader :zip_archive, :zip_entry
+ attr_reader :path, :params
+
+ def initialize(zip_archive, zip_entry, params)
+ @zip_archive = zip_archive
+ @zip_entry = zip_entry
+ @params = params
+ @path = ::File.expand_path(zip_entry.name, params.extract_path)
+ end
+
+ def path_dir
+ ::File.dirname(path)
+ end
+
+ def real_path_dir
+ ::File.realpath(path_dir)
+ end
+
+ def exist?
+ ::File.exist?(path)
+ end
+
+ def extract
+ # do not extract if file is not part of target directory
+ return false unless matching_target_directory
+
+ # do not overwrite existing file
+ raise SafeZip::Extract::AlreadyExistsError, "File already exists #{zip_entry.name}" if exist?
+
+ create_path_dir
+
+ if zip_entry.file?
+ extract_file
+ elsif zip_entry.directory?
+ extract_dir
+ elsif zip_entry.symlink?
+ extract_symlink
+ else
+ raise SafeZip::Extract::UnsupportedEntryError, "File #{zip_entry.name} cannot be extracted"
+ end
+ rescue SafeZip::Extract::Error
+ raise
+ rescue => e
+ raise SafeZip::Extract::ExtractError, e.message
+ end
+
+ private
+
+ def extract_file
+ zip_archive.extract(zip_entry, path)
+ end
+
+ def extract_dir
+ FileUtils.mkdir(path)
+ end
+
+ def extract_symlink
+ source_path = read_symlink
+ real_source_path = expand_symlink(source_path)
+
+ # ensure that source path of symlink is within target directories
+ unless real_source_path.start_with?(matching_target_directory)
+ raise SafeZip::Extract::PermissionDeniedError, "Symlink cannot be created targeting: #{source_path}"
+ end
+
+ ::File.symlink(source_path, path)
+ end
+
+ def create_path_dir
+ # Create all directories, but ignore permissions
+ FileUtils.mkdir_p(path_dir)
+
+ # disallow to make path dirs to point to another directories
+ unless path_dir == real_path_dir
+ raise SafeZip::Extract::PermissionDeniedError, "Directory of #{zip_entry.name} points to another directory"
+ end
+ end
+
+ def matching_target_directory
+ params.matching_target_directory(path)
+ end
+
+ def read_symlink
+ zip_archive.read(zip_entry)
+ end
+
+ def expand_symlink(source_path)
+ ::File.realpath(source_path, path_dir)
+ rescue
+ raise SafeZip::Extract::SymlinkSourceDoesNotExistError, "Symlink source #{source_path} does not exist"
+ end
+ end
+end
diff --git a/lib/safe_zip/extract.rb b/lib/safe_zip/extract.rb
new file mode 100644
index 00000000000..679c021c730
--- /dev/null
+++ b/lib/safe_zip/extract.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module SafeZip
+ class Extract
+ Error = Class.new(StandardError)
+ PermissionDeniedError = Class.new(Error)
+ SymlinkSourceDoesNotExistError = Class.new(Error)
+ UnsupportedEntryError = Class.new(Error)
+ AlreadyExistsError = Class.new(Error)
+ NoMatchingError = Class.new(Error)
+ ExtractError = Class.new(Error)
+
+ attr_reader :archive_path
+
+ def initialize(archive_file)
+ @archive_path = archive_file
+ end
+
+ def extract(opts = {})
+ params = SafeZip::ExtractParams.new(**opts)
+
+ if Feature.enabled?(:safezip_use_rubyzip, default_enabled: true)
+ extract_with_ruby_zip(params)
+ else
+ legacy_unsafe_extract_with_system_zip(params)
+ end
+ end
+
+ private
+
+ def extract_with_ruby_zip(params)
+ ::Zip::File.open(archive_path) do |zip_archive|
+ # Extract all files in the following order:
+ # 1. Directories first,
+ # 2. Files next,
+ # 3. Symlinks last (or anything else)
+ extracted = extract_all_entries(zip_archive, params,
+ zip_archive.lazy.select(&:directory?))
+
+ extracted += extract_all_entries(zip_archive, params,
+ zip_archive.lazy.select(&:file?))
+
+ extracted += extract_all_entries(zip_archive, params,
+ zip_archive.lazy.reject(&:directory?).reject(&:file?))
+
+ raise NoMatchingError, 'No entries extracted' unless extracted > 0
+ end
+ end
+
+ def extract_all_entries(zip_archive, params, entries)
+ entries.count do |zip_entry|
+ SafeZip::Entry.new(zip_archive, zip_entry, params)
+ .extract
+ end
+ end
+
+ def legacy_unsafe_extract_with_system_zip(params)
+ # Requires UnZip at least 6.00 Info-ZIP.
+ # -n never overwrite existing files
+ args = %W(unzip -n -qq #{archive_path})
+
+ # We add * to end of directory, because we want to extract directory and all subdirectories
+ args += params.directories_wildcard
+
+ # Target directory where we extract
+ args += %W(-d #{params.extract_path})
+
+ unless system(*args)
+ raise Error, 'archive failed to extract'
+ end
+ end
+ end
+end
diff --git a/lib/safe_zip/extract_params.rb b/lib/safe_zip/extract_params.rb
new file mode 100644
index 00000000000..bd3b788bac9
--- /dev/null
+++ b/lib/safe_zip/extract_params.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module SafeZip
+ class ExtractParams
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :directories, :extract_path
+
+ def initialize(directories:, to:)
+ @directories = directories
+ @extract_path = ::File.realpath(to)
+ end
+
+ def matching_target_directory(path)
+ target_directories.find do |directory|
+ path.start_with?(directory)
+ end
+ end
+
+ def target_directories
+ strong_memoize(:target_directories) do
+ directories.map do |directory|
+ ::File.join(::File.expand_path(directory, extract_path), '')
+ end
+ end
+ end
+
+ def directories_wildcard
+ strong_memoize(:directories_wildcard) do
+ directories.map do |directory|
+ ::File.join(directory, '*')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 343f2c49a7f..4187014d49e 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -3,6 +3,7 @@
module Sentry
class Client
Error = Class.new(StandardError)
+ SentryError = Class.new(StandardError)
attr_accessor :url, :token
@@ -16,6 +17,13 @@ module Sentry
map_to_errors(issues)
end
+ def list_projects
+ projects = get_projects
+ map_to_projects(projects)
+ rescue KeyError => e
+ raise Client::SentryError, "Sentry API response is missing keys. #{e.message}"
+ end
+
private
def request_params
@@ -27,18 +35,23 @@ module Sentry
}
end
- def get_issues(issue_status:, limit:)
- resp = Gitlab::HTTP.get(
- issues_api_url,
- **request_params.merge(query: {
- query: "is:#{issue_status}",
- limit: limit
- })
- )
+ def http_get(url, params = {})
+ resp = Gitlab::HTTP.get(url, **request_params.merge(params))
handle_response(resp)
end
+ def get_issues(issue_status:, limit:)
+ http_get(issues_api_url, query: {
+ query: "is:#{issue_status}",
+ limit: limit
+ })
+ end
+
+ def get_projects
+ http_get(projects_api_url)
+ end
+
def handle_response(response)
unless response.code == 200
raise Client::Error, "Sentry response error: #{response.code}"
@@ -47,6 +60,13 @@ module Sentry
response.as_json
end
+ def projects_api_url
+ projects_url = URI(@url)
+ projects_url.path = '/api/0/projects/'
+
+ projects_url
+ end
+
def issues_api_url
issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/')
@@ -55,9 +75,11 @@ module Sentry
end
def map_to_errors(issues)
- issues.map do |issue|
- map_to_error(issue)
- end
+ issues.map(&method(:map_to_error))
+ end
+
+ def map_to_projects(projects)
+ projects.map(&method(:map_to_project))
end
def issue_url(id)
@@ -100,5 +122,19 @@ module Sentry
project_slug: project.fetch('slug', nil)
)
end
+
+ def map_to_project(project)
+ organization = project.fetch('organization')
+
+ Gitlab::ErrorTracking::Project.new(
+ id: project.fetch('id'),
+ name: project.fetch('name'),
+ slug: project.fetch('slug'),
+ status: project.dig('status'),
+ organization_name: organization.fetch('name'),
+ organization_id: organization.fetch('id'),
+ organization_slug: organization.fetch('slug')
+ )
+ end
end
end
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index 09dc3aa9882..f9ce3e1d338 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -37,7 +37,7 @@ namespace :gitlab do
print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}"
helper.project_id_batches do |start, finish|
- storage_migrator.bulk_schedule(start, finish)
+ storage_migrator.bulk_schedule(start: start, finish: finish)
print '.'
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f0316234dc6..bb98fc06ed6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -504,6 +504,9 @@ msgstr ""
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr ""
+msgid "All issues for this milestone are closed. You may close this milestone now."
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -528,6 +531,9 @@ msgstr ""
msgid "Allow users to request access if visibility is public or internal."
msgstr ""
+msgid "Allowed to fail"
+msgstr ""
+
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
@@ -714,6 +720,9 @@ msgstr ""
msgid "Are you sure you want to lose unsaved changes?"
msgstr ""
+msgid "Are you sure you want to lose your issue information?"
+msgstr ""
+
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr ""
@@ -759,6 +768,9 @@ msgstr ""
msgid "Assign milestone"
msgstr ""
+msgid "Assign some issues to this milestone."
+msgstr ""
+
msgid "Assign to"
msgstr ""
@@ -1179,6 +1191,9 @@ msgstr ""
msgid "CI / CD Settings"
msgstr ""
+msgid "CI Lint"
+msgstr ""
+
msgid "CI/CD"
msgstr ""
@@ -1248,6 +1263,12 @@ msgstr ""
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
+msgid "Certificate"
+msgstr ""
+
+msgid "Certificate (PEM)"
+msgstr ""
+
msgid "Change permissions"
msgstr ""
@@ -1287,6 +1308,9 @@ msgstr ""
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr ""
+msgid "Check your .gitlab-ci.yml"
+msgstr ""
+
msgid "Checking %{text} availability…"
msgstr ""
@@ -1428,6 +1452,9 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
+msgid "Clear"
+msgstr ""
+
msgid "Clear search"
msgstr ""
@@ -1473,9 +1500,15 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Close milestone"
+msgstr ""
+
msgid "Closed"
msgstr ""
+msgid "Closed (moved)"
+msgstr ""
+
msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}"
msgstr ""
@@ -1521,6 +1554,9 @@ msgstr ""
msgid "ClusterIntegration|Applications"
msgstr ""
+msgid "ClusterIntegration|Apply for credit"
+msgstr ""
+
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
@@ -2018,6 +2054,9 @@ msgstr ""
msgid "Compare Revisions"
msgstr ""
+msgid "Compare changes"
+msgstr ""
+
msgid "Compare changes with the last commit"
msgstr ""
@@ -2117,6 +2156,9 @@ msgstr ""
msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
msgstr ""
+msgid "Contents of .gitlab-ci.yml"
+msgstr ""
+
msgid "Continue"
msgstr ""
@@ -2216,6 +2258,9 @@ msgstr ""
msgid "Create New Directory"
msgstr ""
+msgid "Create New Domain"
+msgstr ""
+
msgid "Create a new branch"
msgstr ""
@@ -2261,6 +2306,9 @@ msgstr ""
msgid "Create merge request and branch"
msgstr ""
+msgid "Create milestone"
+msgstr ""
+
msgid "Create new branch"
msgstr ""
@@ -2366,6 +2414,9 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "DNS"
+msgstr ""
+
msgid "Dashboard"
msgstr ""
@@ -2674,6 +2725,9 @@ msgstr ""
msgid "Download"
msgstr ""
+msgid "Download artifacts"
+msgstr ""
+
msgid "Download asset"
msgstr ""
@@ -2716,6 +2770,9 @@ msgstr ""
msgid "Edit Label"
msgstr ""
+msgid "Edit Milestone"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
@@ -2836,6 +2893,9 @@ msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
msgstr ""
+msgid "Environment:"
+msgstr ""
+
msgid "Environments"
msgstr ""
@@ -2989,6 +3049,9 @@ msgstr ""
msgid "Error while loading the merge request. Please try again."
msgstr ""
+msgid "Error:"
+msgstr ""
+
msgid "Errors"
msgstr ""
@@ -3034,6 +3097,9 @@ msgstr ""
msgid "Everyone can contribute"
msgstr ""
+msgid "Except policy:"
+msgstr ""
+
msgid "Existing Git repository"
msgstr ""
@@ -3085,6 +3151,9 @@ msgstr ""
msgid "External URL"
msgstr ""
+msgid "External Wiki"
+msgstr ""
+
msgid "Facebook"
msgstr ""
@@ -3142,6 +3211,9 @@ msgstr ""
msgid "File added"
msgstr ""
+msgid "File browser"
+msgstr ""
+
msgid "File deleted"
msgstr ""
@@ -3166,6 +3238,9 @@ msgstr ""
msgid "Filter by commit message"
msgstr ""
+msgid "Filter by milestone name"
+msgstr ""
+
msgid "Filter by two-factor authentication"
msgstr ""
@@ -3594,9 +3669,6 @@ msgstr[1] ""
msgid "Hide values"
msgstr ""
-msgid "Hide whitespace changes"
-msgstr ""
-
msgid "History"
msgstr ""
@@ -3885,6 +3957,12 @@ msgstr ""
msgid "Job has been erased"
msgstr ""
+msgid "Job is stuck. Check runners."
+msgstr ""
+
+msgid "Job was retried"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -3942,12 +4020,18 @@ msgstr ""
msgid "June"
msgstr ""
+msgid "Key (PEM)"
+msgstr ""
+
msgid "Kubernetes"
msgstr ""
msgid "Kubernetes Cluster"
msgstr ""
+msgid "Kubernetes Clusters"
+msgstr ""
+
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr ""
@@ -4288,6 +4372,9 @@ msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
+msgid "MergeRequests|Jump to next unresolved discussion"
+msgstr ""
+
msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr ""
@@ -4303,6 +4390,9 @@ msgstr ""
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
+msgid "MergeRequests|commented on commit %{commitLink}"
+msgstr ""
+
msgid "MergeRequests|started a discussion"
msgstr ""
@@ -4515,6 +4605,12 @@ msgstr[1] ""
msgid "New Label"
msgstr ""
+msgid "New Milestone"
+msgstr ""
+
+msgid "New Pages Domain"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -4554,6 +4650,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New milestone"
+msgstr ""
+
msgid "New pipelines will cancel older, pending pipelines on the same branch"
msgstr ""
@@ -4635,6 +4734,9 @@ msgstr ""
msgid "No messages were logged"
msgstr ""
+msgid "No milestones to show"
+msgstr ""
+
msgid "No other labels with such name or description"
msgstr ""
@@ -4814,6 +4916,9 @@ msgstr ""
msgid "Only mirror protected branches"
msgstr ""
+msgid "Only policy:"
+msgstr ""
+
msgid "Only project members can comment."
msgstr ""
@@ -4889,6 +4994,12 @@ msgstr ""
msgid "Pages"
msgstr ""
+msgid "Pages Domain"
+msgstr ""
+
+msgid "Pages Domains"
+msgstr ""
+
msgid "Pagination|Last »"
msgstr ""
@@ -4901,12 +5012,18 @@ msgstr ""
msgid "Pagination|« First"
msgstr ""
+msgid "Parameter"
+msgstr ""
+
msgid "Part of merge request changes"
msgstr ""
msgid "Password"
msgstr ""
+msgid "Past due"
+msgstr ""
+
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
msgstr ""
@@ -5815,6 +5932,9 @@ msgstr ""
msgid "Rename folder"
msgstr ""
+msgid "Reopen milestone"
+msgstr ""
+
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr ""
@@ -6027,6 +6147,9 @@ msgstr ""
msgid "Save"
msgstr ""
+msgid "Save Changes"
+msgstr ""
+
msgid "Save application"
msgstr ""
@@ -6659,6 +6782,9 @@ msgstr ""
msgid "Status"
msgstr ""
+msgid "Status:"
+msgstr ""
+
msgid "Stop environment"
msgstr ""
@@ -6716,6 +6842,9 @@ msgstr ""
msgid "Suggested change"
msgstr ""
+msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it."
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -6734,6 +6863,9 @@ msgstr ""
msgid "Tag"
msgstr ""
+msgid "Tag list:"
+msgstr ""
+
msgid "Tags"
msgstr ""
@@ -7025,6 +7157,9 @@ msgstr ""
msgid "This directory"
msgstr ""
+msgid "This domain is not verified. You will need to verify ownership before access is enabled."
+msgstr ""
+
msgid "This group"
msgstr ""
@@ -7353,9 +7488,15 @@ msgstr ""
msgid "Titles and Filenames"
msgstr ""
+msgid "To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration."
+msgstr ""
+
msgid "To GitLab"
msgstr ""
+msgid "To access this domain create a new DNS record"
+msgstr ""
+
msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr ""
@@ -7593,6 +7734,12 @@ msgstr ""
msgid "Upload New File"
msgstr ""
+msgid "Upload a certificate for your domain with all intermediates"
+msgstr ""
+
+msgid "Upload a private key for your certificate"
+msgstr ""
+
msgid "Upload file"
msgstr ""
@@ -7692,6 +7839,15 @@ msgstr ""
msgid "Users requesting access to"
msgstr ""
+msgid "Validate"
+msgstr ""
+
+msgid "Validate your GitLab CI configuration file"
+msgstr ""
+
+msgid "Value"
+msgstr ""
+
msgid "Various container registry settings."
msgstr ""
@@ -7701,6 +7857,9 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
+msgid "Verification status"
+msgstr ""
+
msgid "Verified"
msgstr ""
@@ -7797,15 +7956,15 @@ msgstr ""
msgid "Web terminal"
msgstr ""
-msgid "What's new?"
-msgstr ""
-
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
+msgid "When:"
+msgstr ""
+
msgid "Who can see this group?"
msgstr ""
@@ -7956,6 +8115,9 @@ msgstr ""
msgid "Write a comment or drag your files here…"
msgstr ""
+msgid "Write milestone description..."
+msgstr ""
+
msgid "Yes"
msgstr ""
@@ -8172,6 +8334,9 @@ msgstr ""
msgid "ago"
msgstr ""
+msgid "allowed to fail"
+msgstr ""
+
msgid "among other things"
msgstr ""
@@ -8290,11 +8455,17 @@ msgstr ""
msgid "latest version"
msgstr ""
+msgid "manual"
+msgstr ""
+
msgid "merge request"
msgid_plural "merge requests"
msgstr[0] ""
msgstr[1] ""
+msgid "missing"
+msgstr ""
+
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr ""
@@ -8496,6 +8667,9 @@ msgstr ""
msgid "new merge request"
msgstr ""
+msgid "none"
+msgstr ""
+
msgid "notification emails"
msgstr ""
@@ -8568,9 +8742,18 @@ msgstr ""
msgid "stuck"
msgstr ""
+msgid "syntax is correct"
+msgstr ""
+
+msgid "syntax is incorrect"
+msgstr ""
+
msgid "this document"
msgstr ""
+msgid "triggered"
+msgstr ""
+
msgid "updated"
msgstr ""
@@ -8580,6 +8763,9 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!"
msgstr ""
+msgid "verify ownership"
+msgstr ""
+
msgid "view it on GitLab"
msgstr ""
diff --git a/package.json b/package.json
index 6c771e377b8..13c0527c4a3 100644
--- a/package.json
+++ b/package.json
@@ -28,8 +28,8 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.8.0",
- "@gitlab/svgs": "^1.47.0",
- "@gitlab/ui": "^1.20.0",
+ "@gitlab/svgs": "^1.48.0",
+ "@gitlab/ui": "^1.22.1",
"apollo-boost": "^0.1.20",
"apollo-client": "^2.4.5",
"autosize": "^4.0.0",
@@ -79,12 +79,14 @@
"katex": "^0.10.0",
"marked": "^0.3.12",
"mermaid": "^8.0.0-rc.8",
- "monaco-editor": "^0.14.3",
- "monaco-editor-webpack-plugin": "^1.5.4",
+ "monaco-editor": "^0.15.6",
+ "monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
"pikaday": "^1.6.1",
"popper.js": "^1.14.3",
"prismjs": "^1.6.0",
+ "prosemirror-markdown": "^1.3.0",
+ "prosemirror-model": "^1.6.4",
"raphael": "^2.2.7",
"raven-js": "^3.22.1",
"raw-loader": "^1.0.0",
@@ -101,6 +103,9 @@
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
"timeago.js": "^3.0.2",
+ "tiptap": "^1.8.0",
+ "tiptap-commands": "^1.4.0",
+ "tiptap-extensions": "^1.8.0",
"underscore": "^1.9.0",
"url-loader": "^1.1.2",
"visibilityjs": "^1.2.4",
@@ -112,7 +117,7 @@
"vue-template-compiler": "^2.5.21",
"vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
- "webpack": "^4.28.1",
+ "webpack": "^4.29.0",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.1",
"webpack-stats-plugin": "^0.2.1",
@@ -160,12 +165,12 @@
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^4.0.0-beta.0",
- "nodemon": "^1.18.4",
+ "nodemon": "^1.18.9",
"pixelmatch": "^4.0.2",
"prettier": "1.16.1",
"vue-jest": "^3.0.2",
"webpack-dev-server": "^3.1.14",
- "yarn-deduplicate": "^1.0.5"
+ "yarn-deduplicate": "^1.1.0"
},
"engines": {
"node": ">=8.10.0",
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 419cacdb2af..9f84bdc3828 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -97,7 +97,7 @@ DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
- nokogiri (~> 1.10.0)
+ nokogiri (~> 1.10.1)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/Rakefile b/qa/Rakefile
index 8df1cfdc174..9a7b9c6bb35 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -1,6 +1,12 @@
require_relative 'qa/tools/revoke_all_personal_access_tokens'
+require_relative 'qa/tools/delete_subgroups'
desc "Revokes all personal access tokens"
task :revoke_personal_access_tokens do
QA::Tools::RevokeAllPersonalAccessTokens.new.run
end
+
+desc "Deletes subgroups within a provided group"
+task :delete_subgroups do
+ QA::Tools::DeleteSubgroups.new.run
+end
diff --git a/qa/qa.rb b/qa/qa.rb
index 2cbd74121f1..355034daec9 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -196,8 +196,12 @@ module QA
end
module SubMenus
+ autoload :CiCd, 'qa/page/project/sub_menus/ci_cd'
autoload :Common, 'qa/page/project/sub_menus/common'
+ autoload :Issues, 'qa/page/project/sub_menus/issues'
+ autoload :Operations, 'qa/page/project/sub_menus/operations'
autoload :Repository, 'qa/page/project/sub_menus/repository'
+ autoload :Settings, 'qa/page/project/sub_menus/settings'
end
module Issue
@@ -286,6 +290,7 @@ module QA
#
module Component
autoload :ClonePanel, 'qa/page/component/clone_panel'
+ autoload :LazyLoader, 'qa/page/component/lazy_loader'
autoload :LegacyClonePanel, 'qa/page/component/legacy_clone_panel'
autoload :Dropzone, 'qa/page/component/dropzone'
autoload :GroupsFilter, 'qa/page/component/groups_filter'
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index ac8dcbf0d83..0aa94101098 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -5,15 +5,19 @@ require 'uri'
require 'open3'
require 'fileutils'
require 'tmpdir'
+require 'tempfile'
+require 'securerandom'
module QA
module Git
class Repository
include Scenario::Actable
- attr_writer :password, :use_lfs
+ attr_writer :use_lfs
attr_accessor :env_vars
+ InvalidCredentialsError = Class.new(RuntimeError)
+
def initialize
# We set HOME to the current working directory (which is a
# temporary directory created in .perform()) so the temporarily dropped
@@ -28,6 +32,14 @@ module QA
end
end
+ def password=(password)
+ @password = password
+
+ raise InvalidCredentialsError, "Please provide a username when setting a password" unless username
+
+ try_add_credentials_to_netrc
+ end
+
def uri=(address)
@uri = URI(address)
end
@@ -148,16 +160,7 @@ module QA
return unless add_credentials?
return if netrc_already_contains_content?
- # Despite libcurl supporting a custom .netrc location through the
- # CURLOPT_NETRC_FILE environment variable, git does not support it :(
- # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html
- #
- # This will create a .netrc in the correct working directory, which is
- # a temporary directory created in .perform()
- #
- FileUtils.mkdir_p(tmp_home_dir)
- File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) }
- File.chmod(0600, netrc_file_path)
+ save_netrc_content
end
private
@@ -175,7 +178,6 @@ module QA
def add_credentials?
return false if !username || !password
return true unless ssh_key_set?
- return true if ssh_key_set? && use_lfs?
false
end
@@ -214,6 +216,23 @@ module QA
end
end
+ def read_netrc_content
+ File.exist?(netrc_file_path) ? File.readlines(netrc_file_path) : []
+ end
+
+ def save_netrc_content
+ # Despite libcurl supporting a custom .netrc location through the
+ # CURLOPT_NETRC_FILE environment variable, git does not support it :(
+ # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html
+ #
+ # This will create a .netrc in the correct working directory, which is
+ # a temporary directory created in .perform()
+ #
+ FileUtils.mkdir_p(tmp_home_dir)
+ File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) }
+ File.chmod(0600, netrc_file_path)
+ end
+
def tmp_home_dir
@tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s)
end
@@ -227,8 +246,7 @@ module QA
end
def netrc_already_contains_content?
- File.exist?(netrc_file_path) &&
- File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any?
+ read_netrc_content.grep(/^#{netrc_content}$/).any?
end
end
end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index c3c90f254b7..b1f27131207 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -128,6 +128,10 @@ module QA
page.has_no_text? text
end
+ def finished_loading?
+ has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
+ end
+
def within_element(name)
page.within(element_selector_css(name)) do
yield
diff --git a/qa/qa/page/component/lazy_loader.rb b/qa/qa/page/component/lazy_loader.rb
new file mode 100644
index 00000000000..6f74a4691ba
--- /dev/null
+++ b/qa/qa/page/component/lazy_loader.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module LazyLoader
+ def self.included(base)
+ base.view 'app/assets/javascripts/lazy_loader.js' do
+ element :js_lazy_loaded
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 0f0ab81a4ef..6dd9ff997a4 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -6,7 +6,7 @@ module QA
class Show < Page::Base
include Page::Component::GroupsFilter
- view 'app/views/groups/show.html.haml' do
+ view 'app/views/groups/_home_panel.html.haml' do
element :new_project_or_subgroup_dropdown
element :new_project_or_subgroup_dropdown_toggle
element :new_project_option
diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb
index 97ce8f0eba5..f0d323ca3b4 100644
--- a/qa/qa/page/label/index.rb
+++ b/qa/qa/page/label/index.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module QA
module Page
module Label
class Index < Page::Base
+ include Component::LazyLoader
+
view 'app/views/shared/labels/_nav.html.haml' do
element :label_create_new
end
@@ -10,10 +14,6 @@ module QA
element :label_svg
end
- view 'app/assets/javascripts/lazy_loader.js' do
- element :js_lazy_loaded
- end
-
def go_to_new_label
# The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 6804cc8fb20..616d50f47fc 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -57,8 +57,12 @@ module QA
end
def go_to_profile_settings
- within_user_menu do
- click_link 'Settings'
+ with_retry(reload: false) do
+ within_user_menu do
+ click_link 'Settings'
+ end
+
+ has_text?('User Settings')
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 4f21ed602d9..f54bea880a0 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -50,17 +50,17 @@ module QA
end
def fast_forward_possible?
- !has_text?('Fast-forward merge is not possible')
+ has_no_text?('Fast-forward merge is not possible')
end
def has_merge_button?
refresh
- has_css?(element_selector_css(:merge_button))
+ has_element?(:merge_button)
end
def has_merge_options?
- has_css?(element_selector_css(:merge_moment_dropdown))
+ has_element?(:merge_moment_dropdown)
end
def merge_immediately
@@ -75,19 +75,21 @@ module QA
def rebase!
# The rebase button is disabled on load
wait do
- has_css?(element_selector_css(:mr_rebase_button))
+ has_element?(:mr_rebase_button)
end
# The rebase button is enabled via JS
wait(reload: false) do
- !first(element_selector_css(:mr_rebase_button)).disabled?
+ !find_element(:mr_rebase_button).disabled?
end
click_element :mr_rebase_button
- wait(reload: false) do
+ success = wait do
has_text?('Fast-forward merge without a merge commit')
end
+
+ raise "Rebase did not appear to be successful" unless success
end
def has_assignee?(username)
@@ -106,30 +108,32 @@ module QA
def merge!
# The merge button is disabled on load
wait do
- has_css?(element_selector_css(:merge_button))
+ has_element?(:merge_button)
end
# The merge button is enabled via JS
wait(reload: false) do
- !first(element_selector_css(:merge_button)).disabled?
+ !find_element(:merge_button).disabled?
end
merge_immediately
- wait(reload: false) do
+ success = wait do
has_text?('The changes were merged into')
end
+
+ raise "Merge did not appear to be successful" unless success
end
def mark_to_squash
# The squash checkbox is disabled on load
wait do
- has_css?(element_selector_css(:squash_checkbox))
+ has_element?(:squash_checkbox)
end
# The squash checkbox is enabled via JS
wait(reload: false) do
- !first(element_selector_css(:squash_checkbox)).disabled?
+ !find_element(:squash_checkbox).disabled?
end
click_element :squash_checkbox
@@ -145,7 +149,7 @@ module QA
def add_comment_to_diff(text)
wait(time: 5) do
- page.has_text?("No newline at end of file")
+ has_text?("No newline at end of file")
end
all_elements(:new_diff_line).first.hover
click_element :diff_comment
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 762a97e2088..922a6ddb086 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -19,10 +19,12 @@ module QA
within_element(:all_branches) do
within(".js-branch-#{branch_name}") do
accept_alert do
- find_element(:remove_btn).click
+ click_element(:remove_btn)
end
end
end
+
+ finished_loading?
end
def has_branch_title?(branch_title)
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index eb3426b1ac0..46dfe87fe25 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -5,148 +5,34 @@ module QA
module Project
class Menu < Page::Base
include SubMenus::Common
+
+ include SubMenus::CiCd
+ include SubMenus::Issues
+ include SubMenus::Operations
include SubMenus::Repository
+ include SubMenus::Settings
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
- element :settings_item
- element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern
- element :link_pipelines
- element :link_operations
- element :link_members_settings
- element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern
- element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern
- element :operations_environments_link
- element :issues_link, /link_to.*shortcuts-issues/ # rubocop:disable QA/ElementWithPattern
- element :issues_link_text, "Issues" # rubocop:disable QA/ElementWithPattern
- element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern
- element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern
- element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern
- element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern
- element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern
- element :milestones_link
- element :labels_link
- end
-
- view 'app/assets/javascripts/fly_out_nav.js' do
- element :fly_out, "classList.add('fly-out-list')" # rubocop:disable QA/ElementWithPattern
- end
-
- def click_ci_cd_pipelines
- within_sidebar do
- click_element :link_pipelines
- end
- end
-
- def click_ci_cd_settings
- hover_settings do
- within_submenu do
- click_link('CI / CD')
- end
- end
- end
-
- def click_issues
- within_sidebar do
- click_link('Issues')
- end
- end
-
- def click_members_settings
- hover_settings do
- within_submenu do
- click_element :link_members_settings
- end
- end
+ element :activity_link
+ element :merge_requests_link
+ element :wiki_link
end
def click_merge_requests
within_sidebar do
- click_link('Merge Requests')
- end
- end
-
- def click_operations_environments
- hover_operations do
- within_submenu do
- click_element(:operations_environments_link)
- end
- end
- end
-
- def click_operations_kubernetes
- hover_operations do
- within_submenu do
- click_link('Kubernetes')
- end
- end
- end
-
- def click_milestones
- within_sidebar do
- click_element :milestones_link
- end
- end
-
- def click_repository_settings
- hover_settings do
- within_submenu do
- click_link('Repository')
- end
+ click_element(:merge_requests_link)
end
end
def click_wiki
within_sidebar do
- click_link('Wiki')
+ click_element(:wiki_link)
end
end
def go_to_activity
within_sidebar do
- click_on 'Activity'
- end
- end
-
- def go_to_labels
- hover_issues do
- within_submenu do
- click_element(:labels_link)
- end
- end
- end
-
- def go_to_settings
- within_sidebar do
- click_on 'Settings'
- end
- end
-
- private
-
- def hover_issues
- within_sidebar do
- scroll_to_element(:issues_item)
- find_element(:issues_item).hover
-
- yield
- end
- end
-
- def hover_operations
- within_sidebar do
- scroll_to_element(:link_operations)
- find_element(:link_operations).hover
-
- yield
- end
- end
-
- def hover_settings
- within_sidebar do
- scroll_to_element(:settings_item)
- find_element(:settings_item).hover
-
- yield
+ click_element(:activity_link)
end
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index 9e8f9ba79d7..98ac5c32d91 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -30,7 +30,7 @@ module QA
def ingress_ip
# We need to wait longer since it can take some time before the
# ip address is assigned for the ingress controller
- page.find('#ingress-ip-address', wait: 500).value
+ page.find('#ingress-ip-address', wait: 1200).value
end
end
end
diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb
new file mode 100644
index 00000000000..adae2ce08c4
--- /dev/null
+++ b/qa/qa/page/project/sub_menus/ci_cd.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module SubMenus
+ module CiCd
+ def self.included(base)
+ base.class_eval do
+ view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ element :link_pipelines
+ end
+ end
+ end
+
+ def click_ci_cd_pipelines
+ within_sidebar do
+ click_element :link_pipelines
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb
new file mode 100644
index 00000000000..f81e4f34909
--- /dev/null
+++ b/qa/qa/page/project/sub_menus/issues.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module SubMenus
+ module Issues
+ def self.included(base)
+ base.class_eval do
+ view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ element :issues_item
+ element :labels_link
+ element :milestones_link
+ end
+ end
+ end
+
+ def click_issues
+ within_sidebar do
+ click_link('Issues')
+ end
+ end
+
+ def click_milestones
+ within_sidebar do
+ click_element :milestones_link
+ end
+ end
+
+ def go_to_labels
+ hover_issues do
+ within_submenu do
+ click_element(:labels_link)
+ end
+ end
+ end
+
+ private
+
+ def hover_issues
+ within_sidebar do
+ scroll_to_element(:issues_item)
+ find_element(:issues_item).hover
+
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb
new file mode 100644
index 00000000000..cf9fc453565
--- /dev/null
+++ b/qa/qa/page/project/sub_menus/operations.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module SubMenus
+ module Operations
+ def self.included(base)
+ base.class_eval do
+ view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ element :link_operations
+ element :operations_environments_link
+ end
+ end
+ end
+
+ def click_operations_environments
+ hover_operations do
+ within_submenu do
+ click_element(:operations_environments_link)
+ end
+ end
+ end
+
+ def click_operations_kubernetes
+ hover_operations do
+ within_submenu do
+ click_link('Kubernetes')
+ end
+ end
+ end
+
+ private
+
+ def hover_operations
+ within_sidebar do
+ scroll_to_element(:link_operations)
+ find_element(:link_operations).hover
+
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb
new file mode 100644
index 00000000000..62c594c0210
--- /dev/null
+++ b/qa/qa/page/project/sub_menus/settings.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module SubMenus
+ module Settings
+ def self.included(base)
+ base.class_eval do
+ view 'app/views/layouts/nav/sidebar/_project.html.haml' do
+ element :settings_item
+ element :link_members_settings
+ end
+ end
+ end
+
+ def click_ci_cd_settings
+ hover_settings do
+ within_submenu do
+ click_link('CI / CD')
+ end
+ end
+ end
+
+ def click_members_settings
+ hover_settings do
+ within_submenu do
+ click_element :link_members_settings
+ end
+ end
+ end
+
+ def click_repository_settings
+ hover_settings do
+ within_submenu do
+ click_link('Repository')
+ end
+ end
+ end
+
+ def go_to_settings
+ within_sidebar do
+ click_on 'Settings'
+ end
+ end
+
+ private
+
+ def hover_settings
+ within_sidebar do
+ scroll_to_element(:settings_item)
+ find_element(:settings_item).hover
+
+ yield
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb
index 2498af8600c..b90e03be36a 100644
--- a/qa/qa/page/project/wiki/new.rb
+++ b/qa/qa/page/project/wiki/new.rb
@@ -1,42 +1,58 @@
+# frozen_string_literal: true
+
module QA
module Page
module Project
module Wiki
class New < Page::Base
+ include Component::LazyLoader
+
view 'app/views/projects/wikis/_form.html.haml' do
- element :wiki_title_textbox, 'text_field :title' # rubocop:disable QA/ElementWithPattern
- element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content" # rubocop:disable QA/ElementWithPattern
- element :wiki_message_textbox, 'text_field :message' # rubocop:disable QA/ElementWithPattern
- element :save_changes_button, 'submit _("Save changes")' # rubocop:disable QA/ElementWithPattern
- element :create_page_button, 'submit s_("Wiki|Create page")' # rubocop:disable QA/ElementWithPattern
+ element :wiki_title_textbox
+ element :wiki_content_textarea
+ element :wiki_message_textbox
+ element :save_changes_button
+ element :create_page_button
end
view 'app/views/shared/empty_states/_wikis.html.haml' do
- element :create_link, 'Create your first page' # rubocop:disable QA/ElementWithPattern
+ element :create_first_page_link
+ end
+
+ view 'app/views/shared/empty_states/_wikis_layout.html.haml' do
+ element :svg_content
end
def go_to_create_first_page
- click_link 'Create your first page'
+ # The svg takes a fraction of a second to load after which the
+ # "Create your first page" button shifts up a bit. This can cause
+ # webdriver to miss the hit so we wait for the svg to load before
+ # clicking the button.
+ within_element(:svg_content) do
+ has_element? :js_lazy_loaded
+ end
+
+ click_element :create_first_page_link
end
def set_title(title)
- fill_in 'wiki_title', with: title
+ fill_element :wiki_title_textbox, title
end
def set_content(content)
- fill_in 'wiki_content', with: content
+ fill_element :wiki_content_textarea, content
end
def set_message(message)
- fill_in 'wiki_message', with: message
+ fill_element :wiki_message_textbox, message
end
def save_changes
- click_on 'Save changes'
+ click_element :save_changes_button
end
def create_new_page
- click_on 'Create page'
+ click_element :create_page_button
end
end
end
diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb
index 32f15547da2..a5827fb6e73 100644
--- a/qa/qa/resource/repository/push.rb
+++ b/qa/qa/resource/repository/push.rb
@@ -67,8 +67,6 @@ module QA
email = user.email
end
- repository.try_add_credentials_to_netrc
-
@output += repository.clone
repository.configure_identity(username, email)
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 43cc737bfb1..3fbcd77dac6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -5,15 +5,15 @@ module QA
describe 'Merge request rebasing' do
it 'user rebases source branch of merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ Page::Main::Login.perform(&:sign_in_using_credentials)
project = Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
project.visit!
- Page::Project::Menu.act { go_to_settings }
- Page::Project::Settings::MergeRequest.act { enable_ff_only }
+ Page::Project::Menu.perform(&:go_to_settings)
+ Page::Project::Settings::MergeRequest.perform(&:enable_ff_only)
merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
@@ -38,7 +38,7 @@ module QA
merge_request.rebase!
expect(merge_request).to have_merge_button
- expect(merge_request.fast_forward_possible?).to be_truthy
+ expect(merge_request).to be_fast_forward_possible
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index 0f0c627d79a..3567ddca1a1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -28,6 +28,7 @@ module QA
Git::Repository.perform do |repository|
repository.uri = project.repository_http_location.uri
repository.use_default_credentials
+ repository.try_add_credentials_to_netrc
repository.act do
clone
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb
index 621cca0f9a5..b862a7bd1ed 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb
@@ -2,7 +2,10 @@
module QA
context 'Create' do
- describe 'Commit data' do
+ # failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42
+ # also failing in staging until the fix is picked into the next release:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24533
+ describe 'Commit data', :quarantine do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index a7d0998d42c..29589ec870a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -3,22 +3,15 @@
module QA
context 'Create' do
describe 'Wiki management' do
- def login
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
- end
-
def validate_content(content)
expect(page).to have_content('Wiki was successfully updated')
expect(page).to have_content(/#{content}/)
end
- before do
- login
- end
+ it 'user creates, edits, clones, and pushes to the wiki' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- # Failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/24
- it 'user creates, edits, clones, and pushes to the wiki', :quarantine do
wiki = Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
@@ -27,7 +20,7 @@ module QA
validate_content('My First Wiki Content')
- Page::Project::Wiki::Edit.act { go_to_edit_page }
+ Page::Project::Wiki::Edit.perform(&:go_to_edit_page)
Page::Project::Wiki::New.perform do |page|
page.set_content("My Second Wiki Content")
page.save_changes
@@ -41,7 +34,7 @@ module QA
push.file_content = '# My Third Wiki Content'
push.commit_message = 'Update Home.md'
end
- Page::Project::Menu.act { click_wiki }
+ Page::Project::Menu.perform(&:click_wiki)
expect(page).to have_content('My Third Wiki Content')
end
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 7283468d66b..553550eef8b 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
@@ -3,9 +3,7 @@
require 'pathname'
module QA
- # Issues for transient failure:
- # https://gitlab.com/gitlab-org/quality/nightly/issues/40
- # https://gitlab.com/gitlab-org/quality/nightly/issues/61
+ # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68
context 'Configure', :orchestrated, :kubernetes, :quarantine do
describe 'Auto DevOps support' do
def login
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index 1107d43161e..8aa7d6812ac 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -20,6 +20,24 @@ module QA
e.response
end
+ def delete(url)
+ RestClient::Request.execute(
+ method: :delete,
+ url: url,
+ verify_ssl: false)
+ rescue RestClient::ExceptionWithResponse => e
+ e.response
+ end
+
+ def head(url)
+ RestClient::Request.execute(
+ method: :head,
+ url: url,
+ verify_ssl: false)
+ rescue RestClient::ExceptionWithResponse => e
+ e.response
+ end
+
def parse_body(response)
JSON.parse(response.body, symbolize_names: true)
end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index e96756642c8..f2cd0194b6b 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -112,6 +112,17 @@ module QA
found
end
+ def finished_loading?
+ log('waiting for loading to complete...')
+ now = Time.now
+
+ loaded = super
+
+ log("loading complete after #{Time.now - now} seconds")
+
+ loaded
+ end
+
def within_element(name)
log("within element :#{name}")
diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb
new file mode 100644
index 00000000000..c5c48e77ade
--- /dev/null
+++ b/qa/qa/tools/delete_subgroups.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require_relative '../../qa'
+
+# This script deletes all subgroups of a group specified by ENV['GROUP_NAME_OR_PATH']
+# Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS
+# Optional environment variable: GROUP_NAME_OR_PATH (defaults to 'gitlab-qa-sandbox-group')
+# Run `rake delete_subgroups`
+
+module QA
+ module Tools
+ class DeleteSubgroups
+ include Support::Api
+
+ def initialize
+ raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
+ raise ArgumentError, "Please provide PERSONAL_ACCESS_TOKEN" unless ENV['PERSONAL_ACCESS_TOKEN']
+
+ @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN'])
+ end
+
+ def run
+ STDOUT.puts 'Running...'
+
+ # Fetch group's id
+ group_id = fetch_group_id
+
+ sub_groups_head_response = head Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url
+ total_sub_groups = sub_groups_head_response.headers[:x_total]
+ total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages]
+
+ STDOUT.puts "total_sub_groups: #{total_sub_groups}"
+ STDOUT.puts "total_sub_group_pages: #{total_sub_group_pages}"
+
+ total_sub_group_pages.to_i.times do |page_no|
+ # Fetch all subgroups for the top level group
+ sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url
+
+ sub_group_ids = JSON.parse(sub_groups_response.body).map { |subgroup| subgroup["id"] }
+
+ if sub_group_ids.any?
+ STDOUT.puts "\n==== Current Page: #{page_no + 1} ====\n"
+
+ delete_subgroups(sub_group_ids)
+ end
+ end
+ STDOUT.puts "\nDone"
+ end
+
+ private
+
+ def delete_subgroups(sub_group_ids)
+ sub_group_ids.each do |subgroup_id|
+ delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url
+ dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"
+ print dot_or_f
+ end
+ end
+
+ def fetch_group_id
+ group_search_response = get Runtime::API::Request.new(@api_client, "/groups", search: ENV['GROUP_NAME_OR_PATH'] || 'gitlab-qa-sandbox-group').url
+ JSON.parse(group_search_response.body).first["id"]
+ end
+ end
+ end
+end
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index faa154c78da..4a350cd6c42 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -1,69 +1,119 @@
describe QA::Git::Repository do
include Support::StubENV
- let(:repository) { described_class.new }
+ shared_context 'git directory' do
+ let(:repository) { described_class.new }
+ let(:tmp_git_dir) { Dir.mktmpdir }
+ let(:tmp_netrc_dir) { Dir.mktmpdir }
- before do
- stub_env('GITLAB_USERNAME', 'root')
- cd_empty_temp_directory
- set_bad_uri
- repository.use_default_credentials
- end
+ before do
+ stub_env('GITLAB_USERNAME', 'root')
+ cd_empty_temp_directory
+ set_bad_uri
- describe '#clone' do
- it 'is unable to resolve host' do
- expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'")
+ allow(repository).to receive(:tmp_home_dir).and_return(tmp_netrc_dir)
end
- end
- describe '#push_changes' do
- before do
- `git init` # need a repo to push from
+ after do
+ # Switch to a safe dir before deleting tmp dirs to avoid dir access errors
+ FileUtils.cd __dir__
+ FileUtils.remove_entry_secure(tmp_git_dir, true)
+ FileUtils.remove_entry_secure(tmp_netrc_dir, true)
end
- it 'fails to push changes' do
- expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'")
+ def cd_empty_temp_directory
+ FileUtils.cd tmp_git_dir
+ end
+
+ def set_bad_uri
+ repository.uri = 'http://foo/bar.git'
end
end
- describe '#git_protocol=' do
- [0, 1, 2].each do |version|
- it "configures git to use protocol version #{version}" do
- expect(repository).to receive(:run).with("git config protocol.version #{version}")
- repository.git_protocol = version
+ context 'with default credentials' do
+ include_context 'git directory' do
+ before do
+ repository.use_default_credentials
end
end
- it 'raises an error if the version is unsupported' do
- expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ describe '#clone' do
+ it 'is unable to resolve host' do
+ expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'")
+ end
end
- end
- describe '#fetch_supported_git_protocol' do
- it "reports the detected version" do
- expect(repository).to receive(:run).and_return("packet: git< version 2")
- expect(repository.fetch_supported_git_protocol).to eq('2')
+ describe '#push_changes' do
+ before do
+ `git init` # need a repo to push from
+ end
+
+ it 'fails to push changes' do
+ expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'")
+ end
end
- it 'reports unknown if version is unknown' do
- expect(repository).to receive(:run).and_return("packet: git< version -1")
- expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ describe '#git_protocol=' do
+ [0, 1, 2].each do |version|
+ it "configures git to use protocol version #{version}" do
+ expect(repository).to receive(:run).with("git config protocol.version #{version}")
+ repository.git_protocol = version
+ end
+ end
+
+ it 'raises an error if the version is unsupported' do
+ expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ end
end
- it 'reports unknown if content does not identify a version' do
- expect(repository).to receive(:run).and_return("foo")
- expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ describe '#fetch_supported_git_protocol' do
+ it "reports the detected version" do
+ expect(repository).to receive(:run).and_return("packet: git< version 2")
+ expect(repository.fetch_supported_git_protocol).to eq('2')
+ end
+
+ it 'reports unknown if version is unknown' do
+ expect(repository).to receive(:run).and_return("packet: git< version -1")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+
+ it 'reports unknown if content does not identify a version' do
+ expect(repository).to receive(:run).and_return("foo")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
end
- end
- def cd_empty_temp_directory
- tmp_dir = 'tmp/git-repository-spec/'
- FileUtils.rm_rf(tmp_dir) if ::File.exist?(tmp_dir)
- FileUtils.mkdir_p tmp_dir
- FileUtils.cd tmp_dir
+ describe '#use_default_credentials' do
+ it 'adds credentials to .netrc' do
+ expect(File.read(File.join(tmp_netrc_dir, '.netrc')))
+ .to eq("machine foo login #{QA::Runtime::User.default_username} password #{QA::Runtime::User.default_password}\n")
+ end
+ end
end
- def set_bad_uri
- repository.uri = 'http://foo/bar.git'
+ context 'with specific credentials' do
+ include_context 'git directory'
+
+ context 'before setting credentials' do
+ it 'does not add credentials to .netrc' do
+ expect(repository).not_to receive(:save_netrc_content)
+ end
+ end
+
+ describe '#password=' do
+ it 'raises an error if no username was given' do
+ expect { repository.password = 'foo' }
+ .to raise_error(QA::Git::Repository::InvalidCredentialsError,
+ "Please provide a username when setting a password")
+ end
+
+ it 'adds credentials to .netrc' do
+ repository.username = 'user'
+ repository.password = 'foo'
+
+ expect(File.read(File.join(tmp_netrc_dir, '.netrc')))
+ .to eq("machine foo login user password foo\n")
+ end
+ end
end
end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 2eb826becea..f289ee3c2bb 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -1,14 +1,15 @@
# frozen_string_literal: true
require 'capybara/dsl'
+require 'logger'
describe QA::Support::Page::Logging do
include Support::StubENV
- let(:page) { double().as_null_object }
+ let(:page) { double.as_null_object }
before do
- logger = Logger.new $stdout
+ logger = ::Logger.new $stdout
logger.level = ::Logger::DEBUG
QA::Runtime::Logger.logger = logger
@@ -95,6 +96,13 @@ describe QA::Support::Page::Logging do
.to output(/has_no_text\?\('foo'\) returned true/).to_stdout_from_any_process
end
+ it 'logs finished_loading?' do
+ expect { subject.finished_loading? }
+ .to output(/waiting for loading to complete\.\.\./).to_stdout_from_any_process
+ expect { subject.finished_loading? }
+ .to output(/loading complete after .* seconds$/).to_stdout_from_any_process
+ end
+
it 'logs within_element' do
expect { subject.within_element(:element) }
.to output(/within element :element/).to_stdout_from_any_process
diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb
index 044804cd599..4788e0ab46c 100644
--- a/qa/spec/support/stub_env.rb
+++ b/qa/spec/support/stub_env.rb
@@ -19,7 +19,7 @@ module Support
allow(ENV).to receive(:[]).with(key).and_return(value)
allow(ENV).to receive(:key?).with(key).and_return(true)
allow(ENV).to receive(:fetch).with(key).and_return(value)
- allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val|
+ allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val|
value || default_val
end
end
diff --git a/scripts/trigger-build b/scripts/trigger-build
index fbf35e7217c..9dbafffddfc 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -140,6 +140,7 @@ module Trigger
# Back-compatibility until https://gitlab.com/gitlab-org/build/CNG/merge_requests/189 is merged
"GITLAB_#{edition}_VERSION" => ENV['CI_COMMIT_REF_NAME'],
"GITLAB_VERSION" => ENV['CI_COMMIT_REF_NAME'],
+ "GITLAB_TAG" => ENV['CI_COMMIT_TAG'],
"GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_REF_SLUG'],
"#{edition}_PIPELINE" => 'true'
}
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index 5a3a7a15f5a..307c5d60c57 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -17,10 +17,55 @@ describe IssuableCollections do
controller = klass.new
allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
+ allow(controller).to receive(:current_user).and_return(user)
controller
end
+ describe '#set_sort_order_from_user_preference' do
+ describe 'when sort param given' do
+ let(:params) { { sort: 'updated_desc' } }
+
+ context 'when issuable_sorting_field is defined' do
+ before do
+ controller.class.define_method(:issuable_sorting_field) { :issues_sort}
+ end
+
+ it 'sets user_preference with the right value' do
+ controller.send(:set_sort_order_from_user_preference)
+
+ expect(user.user_preference.reload.issues_sort).to eq('updated_desc')
+ end
+ end
+
+ context 'when no issuable_sorting_field is defined on the controller' do
+ it 'does not touch user_preference' do
+ allow(user).to receive(:user_preference)
+
+ controller.send(:set_sort_order_from_user_preference)
+
+ expect(user).not_to have_received(:user_preference)
+ end
+ end
+ end
+
+ context 'when a user sorting preference exists' do
+ let(:params) { {} }
+
+ before do
+ controller.class.define_method(:issuable_sorting_field) { :issues_sort }
+ end
+
+ it 'returns the set preference' do
+ user.user_preference.update(issues_sort: 'updated_asc')
+
+ sort_preference = controller.send(:set_sort_order_from_user_preference)
+
+ expect(sort_preference).to eq('updated_asc')
+ end
+ end
+ end
+
describe '#set_set_order_from_cookie' do
describe 'when sort param given' do
let(:cookies) { {} }
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index c9ccd5f7c55..8b176e07bc8 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -56,6 +56,24 @@ describe Dashboard::MilestonesController do
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
end
+ it 'searches legacy project milestones by title when search_title is given' do
+ project_milestone = create(:milestone, title: 'Project milestone title', project: project)
+
+ get :index, params: { search_title: 'Project mil' }
+
+ expect(response.body).to include(project_milestone.title)
+ expect(response.body).not_to include(group_milestone.title)
+ end
+
+ it 'searches group milestones by title when search_title is given' do
+ group_milestone = create(:milestone, title: 'Group milestone title', group: group)
+
+ get :index, params: { search_title: 'Group mil' }
+
+ expect(response.body).to include(group_milestone.title)
+ expect(response.body).not_to include(project_milestone.title)
+ end
+
it 'should contain group and project milestones to which the user belongs to' do
get :index
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index ed38dadfd6b..3a801fabafc 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -126,7 +126,7 @@ describe Groups::GroupMembersController do
it '[HTML] removes user from members' do
delete :destroy, params: { group_id: group, id: member }
- expect(response).to set_flash.to 'User was successfully removed from group.'
+ expect(response).to set_flash.to 'User was successfully removed from group and any subresources.'
expect(response).to redirect_to(group_group_members_path(group))
expect(group.members).not_to include member
end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 40d991a669c..043cf28514b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -32,10 +32,35 @@ describe Groups::MilestonesController do
end
describe '#index' do
- it 'shows group milestones page' do
- get :index, params: { group_id: group.to_param }
+ describe 'as HTML' do
+ render_views
- expect(response).to have_gitlab_http_status(200)
+ it 'shows group milestones page' do
+ milestone
+
+ get :index, params: { group_id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to include(milestone.title)
+ end
+
+ it 'searches legacy milestones by title when search_title is given' do
+ project_milestone = create(:milestone, project: project, title: 'Project milestone title')
+
+ get :index, params: { group_id: group.to_param, search_title: 'Project mil' }
+
+ expect(response.body).to include(project_milestone.title)
+ expect(response.body).not_to include(milestone.title)
+ end
+
+ it 'searches group milestones by title when search_title is given' do
+ group_milestone = create(:milestone, title: 'Group milestone title', group: group)
+
+ get :index, params: { group_id: group.to_param, search_title: 'Group mil' }
+
+ expect(response.body).to include(group_milestone.title)
+ expect(response.body).not_to include(milestone.title)
+ end
end
context 'as JSON' do
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 51793f2c048..0bc09c86939 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -8,6 +8,7 @@ describe Import::BitbucketController do
let(:secret) { "sekrettt" }
let(:refresh_token) { SecureRandom.hex(15) }
let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
+ let(:code) { SecureRandom.hex(8) }
def assign_session_tokens
session[:bitbucket_token] = token
@@ -32,10 +33,16 @@ describe Import::BitbucketController do
expires_in: expires_in,
refresh_token: refresh_token)
allow_any_instance_of(OAuth2::Client)
- .to receive(:get_token).and_return(access_token)
+ .to receive(:get_token)
+ .with(hash_including(
+ 'grant_type' => 'authorization_code',
+ 'code' => code,
+ redirect_uri: users_import_bitbucket_callback_url),
+ {})
+ .and_return(access_token)
stub_omniauth_provider('bitbucket')
- get :callback
+ get :callback, params: { code: code }
expect(session[:bitbucket_token]).to eq(token)
expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index bb282db5a41..a125e6ed16d 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -28,9 +28,11 @@ describe Import::BitbucketServerController do
end
describe 'POST create' do
+ let(:project_name) { "my-project_123" }
+
before do
allow(controller).to receive(:bitbucket_client).and_return(client)
- repo = double(name: 'my-project')
+ repo = double(name: project_name)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
assign_session_tokens
end
@@ -39,7 +41,7 @@ describe Import::BitbucketServerController do
it 'returns the new project' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
- .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything)
+ .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: project))
post :create, params: { project: project_key, repository: repo_slug }, format: :json
@@ -47,6 +49,20 @@ describe Import::BitbucketServerController do
expect(response).to have_gitlab_http_status(200)
end
+ context 'with project key with tildes' do
+ let(:project_key) { '~someuser_123' }
+
+ it 'successfully creates a project' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
+ .and_return(double(execute: project))
+
+ post :create, params: { project: project_key, repository: repo_slug, format: :json }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
it 'returns an error when an invalid project key is used' do
post :create, params: { project: 'some&project' }
@@ -69,7 +85,7 @@ describe Import::BitbucketServerController do
it 'returns an error when the project cannot be saved' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
- .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything)
+ .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: build(:project)))
post :create, params: { project: project_key, repository: repo_slug }, format: :json
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 780e49f7b93..bca5f3f6589 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -12,9 +12,15 @@ describe Import::GithubController do
it "redirects to GitHub for an access token if logged in with GitHub" do
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
- expect(controller).to receive(:go_to_provider_for_permissions)
+ expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
+ allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
+ .to receive(:authorize_url)
+ .with(users_import_github_callback_url)
+ .and_call_original
get :new
+
+ expect(response).to have_http_status(302)
end
it "prompts for an access token if GitHub not configured" do
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 94fb85f217c..a4d494a820f 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -47,9 +47,43 @@ describe Projects::EnvironmentsController do
let(:environments) { json_response['environments'] }
+ context 'with default parameters' do
+ before do
+ get :index, params: environment_params(format: :json)
+ end
+
+ it 'responds with a flat payload describing available environments' do
+ expect(environments.count).to eq 3
+ expect(environments.first['name']).to eq 'production'
+ expect(environments.second['name']).to eq 'staging/review-1'
+ expect(environments.third['name']).to eq 'staging/review-2'
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
+
+ it 'sets the polling interval header' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Poll-Interval']).to eq("3000")
+ end
+ end
+
+ context 'when a folder-based nested structure is requested' do
+ before do
+ get :index, params: environment_params(format: :json, nested: true)
+ end
+
+ it 'responds with a payload containing the latest environment for each folder' do
+ expect(environments.count).to eq 2
+ expect(environments.first['name']).to eq 'production'
+ expect(environments.second['name']).to eq 'staging'
+ expect(environments.second['size']).to eq 2
+ expect(environments.second['latest']['name']).to eq 'staging/review-2'
+ end
+ end
+
context 'when requesting available environments scope' do
before do
- get :index, params: environment_params(format: :json, scope: :available)
+ get :index, params: environment_params(format: :json, nested: true, scope: :available)
end
it 'responds with a payload describing available environments' do
@@ -64,16 +98,11 @@ describe Projects::EnvironmentsController do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
-
- it 'sets the polling interval header' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Poll-Interval']).to eq("3000")
- end
end
context 'when requesting stopped environments scope' do
before do
- get :index, params: environment_params(format: :json, scope: :stopped)
+ get :index, params: environment_params(format: :json, nested: true, scope: :stopped)
end
it 'responds with a payload describing stopped environments' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index a2c3bb2919d..4743ad04339 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -42,7 +42,9 @@ describe Projects::IssuesController do
it_behaves_like "issuables list meta-data", :issue
- it_behaves_like 'set sort order from user preference'
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'updated_asc' }
+ end
it "returns index" do
get :index, params: { namespace_id: project.namespace, project_id: project }
@@ -66,7 +68,7 @@ describe Projects::IssuesController do
end
context 'with page param' do
- let(:last_page) { project.issues.page().total_pages }
+ let(:last_page) { project.issues.page.total_pages }
let!(:issue_list) { create_list(:issue, 2, project: project) }
before do
@@ -131,7 +133,7 @@ describe Projects::IssuesController do
it 'redirects to signin if not logged in' do
get :new, params: { namespace_id: project.namespace, project_id: project }
- expect(flash[:notice]).to eq 'Please sign in to create the new issue.'
+ expect(flash[:alert]).to eq 'You need to sign in or sign up before continuing.'
expect(response).to redirect_to(new_user_session_path)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 53d5bf752ef..ca5ff9b1e3b 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -153,10 +153,12 @@ describe Projects::MergeRequestsController do
it_behaves_like "issuables list meta-data", :merge_request
- it_behaves_like 'set sort order from user preference'
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'updated_asc' }
+ end
context 'when page param' do
- let(:last_page) { project.merge_requests.page().total_pages }
+ let(:last_page) { project.merge_requests.page.total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
it 'redirects to last_page if page number is larger than number of pages' do
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 5892024e756..ac54b3c3952 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -42,10 +42,11 @@ describe Projects::MilestonesController do
describe "#index" do
context "as html" do
- def render_index(project:, page:)
+ def render_index(project:, page:, search_title: '')
get :index, params: {
namespace_id: project.namespace.id,
project_id: project.id,
+ search_title: search_title,
page: page
}
end
@@ -59,6 +60,15 @@ describe Projects::MilestonesController do
expect(milestones.where(project_id: nil)).to be_empty
end
+ it 'searches milestones by title when search_title is given' do
+ milestone1 = create(:milestone, title: 'Project milestone title', project: project)
+
+ render_index project: project, page: 1, search_title: 'Project mile'
+
+ milestones = assigns(:milestones)
+ expect(milestones).to eq([milestone1])
+ end
+
it 'renders paginated milestones without missing or duplicates' do
allow(Milestone).to receive(:default_per_page).and_return(2)
create_list(:milestone, 5, project: project)
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 80506249ea9..fa732437fc1 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -3,9 +3,14 @@ require 'spec_helper'
describe Projects::PipelineSchedulesController do
include AccessMatchersForController
+ set(:user) { create(:user) }
set(:project) { create(:project, :public, :repository) }
set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ before do
+ project.add_developer(user)
+ end
+
describe 'GET #index' do
render_views
@@ -14,6 +19,10 @@ describe Projects::PipelineSchedulesController do
create(:ci_pipeline_schedule, :inactive, project: project)
end
+ before do
+ sign_in(user)
+ end
+
it 'renders the index view' do
visit_pipelines_schedules
@@ -21,7 +30,7 @@ describe Projects::PipelineSchedulesController do
expect(response).to render_template(:index)
end
- it 'avoids N + 1 queries' do
+ it 'avoids N + 1 queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count
create_list(:ci_pipeline_schedule, 2, project: project)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 97e04a63d4a..ece8532cb84 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::PipelinesController do
set(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
- let(:feature) { ProjectFeature::DISABLED }
+ let(:feature) { ProjectFeature::ENABLED }
before do
stub_not_protect_default_branch
@@ -186,6 +186,27 @@ describe Projects::PipelinesController do
end
end
+ context 'when builds are disabled' do
+ let(:feature) { ProjectFeature::DISABLED }
+
+ it 'users can not see internal pipelines' do
+ get_pipeline_json
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when pipeline is external' do
+ let(:pipeline) { create(:ci_pipeline, source: :external, project: project) }
+
+ it 'users can see the external pipeline' do
+ get_pipeline_json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to be(pipeline.id)
+ end
+ end
+ end
+
def get_pipeline_json
get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json
end
@@ -326,16 +347,14 @@ describe Projects::PipelinesController do
format: :json
end
- context 'when builds are enabled' do
- let(:feature) { ProjectFeature::ENABLED }
-
- it 'retries a pipeline without returning any content' do
- expect(response).to have_gitlab_http_status(:no_content)
- expect(build.reload).to be_retried
- end
+ it 'retries a pipeline without returning any content' do
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(build.reload).to be_retried
end
context 'when builds are disabled' do
+ let(:feature) { ProjectFeature::DISABLED }
+
it 'fails to retry pipeline' do
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -355,16 +374,14 @@ describe Projects::PipelinesController do
format: :json
end
- context 'when builds are enabled' do
- let(:feature) { ProjectFeature::ENABLED }
-
- it 'cancels a pipeline without returning any content' do
- expect(response).to have_gitlab_http_status(:no_content)
- expect(pipeline.reload).to be_canceled
- end
+ it 'cancels a pipeline without returning any content' do
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(pipeline.reload).to be_canceled
end
context 'when builds are disabled' do
+ let(:feature) { ProjectFeature::DISABLED }
+
it 'fails to retry pipeline' do
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index ed0197afcfc..74ed89ba1c3 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::Registry::TagsController do
end
before do
- stub_container_registry_tags(repository: /image/, tags: tags)
+ stub_container_registry_tags(repository: /image/, tags: tags, with_manifest: true)
end
context 'when user can control the registry' do
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 75c9839dd9b..8d9cb2c8ac0 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::SnippetsController do
describe 'GET #index' do
context 'when page param' do
- let(:last_page) { project.snippets.page().total_pages }
+ let(:last_page) { project.snippets.page.total_pages }
let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) }
it 'redirects to last_page if page number is larger than number of pages' do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 27edf226ca3..af61026098b 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -206,6 +206,38 @@ describe UsersController do
end
end
+ describe 'GET #contributed' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+
+ project.add_developer(public_user)
+ project.add_developer(private_user)
+ end
+
+ context 'with public profile' do
+ it 'renders contributed projects' do
+ create(:push_event, project: project, author: public_user)
+
+ get :contributed, params: { username: public_user.username }
+
+ expect(assigns[:contributed_projects]).not_to be_empty
+ end
+ end
+
+ context 'with private profile' do
+ it 'does not render contributed projects' do
+ create(:push_event, project: project, author: private_user)
+
+ get :contributed, params: { username: private_user.username }
+
+ expect(assigns[:contributed_projects]).to be_empty
+ end
+ end
+ end
+
describe 'GET #snippets' do
before do
sign_in(user)
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index 5f83b80ad7b..b1d82b98411 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -10,8 +10,20 @@ FactoryBot.define do
pipeline factory: :ci_pipeline
+ trait :variables do
+ yaml_variables [{ key: 'BRIDGE', value: 'cross', public: true }]
+ end
+
+ transient { downstream nil }
+
after(:build) do |bridge, evaluator|
bridge.project ||= bridge.pipeline.project
+
+ if evaluator.downstream.present?
+ bridge.options = bridge.options.to_h.merge(
+ trigger: { project: evaluator.downstream.full_path }
+ )
+ end
end
end
end
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index 62a89a12ef5..00fad7975c9 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :container_repository do
- name 'test_container_image'
+ name 'test_image'
project
transient do
diff --git a/spec/factories/error_tracking/project.rb b/spec/factories/error_tracking/project.rb
new file mode 100644
index 00000000000..5e9219b241f
--- /dev/null
+++ b/spec/factories/error_tracking/project.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :error_tracking_project, class: Gitlab::ErrorTracking::Project do
+ id '1'
+ name 'Sentry Example'
+ slug 'sentry-example'
+ status 'active'
+ organization_name 'Sentry'
+ organization_id '1'
+ organization_slug 'sentry'
+
+ skip_create
+ end
+end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index d96707e55fd..e42d18b457e 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -112,7 +112,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: 'none')
+ expect(page).to have_selector('.js-visual-token', text: 'None')
expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -147,7 +147,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: 'upcoming')
+ expect(page).to have_selector('.js-visual-token', text: 'Upcoming')
expect(page).to have_selector('.board-card', count: 0)
end
end
@@ -182,7 +182,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: 'none')
+ expect(page).to have_selector('.js-visual-token', text: 'None')
expect(page).to have_selector('.board-card', count: 1)
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 9986206f619..6f9901815e1 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -25,7 +25,7 @@ describe "Container Registry", :js do
context 'when there are image repositories' do
before do
- stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest])
+ stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true)
project.container_repositories << container_repository
end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 0db8093411b..f44bd55ecf6 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -15,7 +15,7 @@ describe 'Tooltips on .timeago dates', :js do
sign_in user
visit user_activity_path(user)
- wait_for_requests()
+ wait_for_requests
page.find('.js-timeago').hover
end
@@ -32,7 +32,7 @@ describe 'Tooltips on .timeago dates', :js do
sign_in user
visit user_snippets_path(user)
- wait_for_requests()
+ wait_for_requests
page.find('.js-timeago.snippet-created-ago').hover
end
diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb
index fa12cecc984..467a503a62d 100644
--- a/spec/features/dashboard/help_spec.rb
+++ b/spec/features/dashboard/help_spec.rb
@@ -5,14 +5,6 @@ RSpec.describe 'Dashboard Help' do
sign_in(create(:user))
end
- context 'help dropdown' do
- it 'shows the "What\'s new?" menu item' do
- visit root_dashboard_path
-
- expect(page.find('.header-help .dropdown-menu')).to have_text("What's new?")
- end
- end
-
context 'documentation' do
it 'renders correctly markdown' do
visit help_page_path("administration/raketasks/maintenance")
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index edca8f9df08..6c4b04ab76b 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -147,6 +147,27 @@ describe 'Dashboard Projects' do
expect(page).to have_link('Commit: passed')
end
end
+
+ context 'guest user of project and project has private pipelines' do
+ let(:guest_user) { create(:user) }
+
+ before do
+ project.update(public_builds: false)
+ project.add_guest(guest_user)
+ sign_in(guest_user)
+ end
+
+ it 'shows that the last pipeline passed' do
+ visit dashboard_projects_path
+
+ page.within('.controls') do
+ expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
+ expect(page).not_to have_css('.ci-status-link')
+ expect(page).not_to have_css('.ci-status-icon-success')
+ expect(page).not_to have_link('Commit: passed')
+ end
+ end
+ end
end
context 'last push widget', :use_clean_rails_memory_store_caching do
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 2cdbdcffbc3..378e4d5febc 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -18,14 +18,14 @@ describe 'Edit group settings' do
update_path(new_group_path)
visit new_group_full_path
expect(current_path).to eq(new_group_full_path)
- expect(find('h1.group-title')).to have_content(group.name)
+ expect(find('h1.home-panel-title')).to have_content(group.name)
end
it 'the old group path redirects to the new path' do
update_path(new_group_path)
visit old_group_full_path
expect(current_path).to eq(new_group_full_path)
- expect(find('h1.group-title')).to have_content(group.name)
+ expect(find('h1.home-panel-title')).to have_content(group.name)
end
context 'with a subgroup' do
@@ -37,14 +37,14 @@ describe 'Edit group settings' do
update_path(new_group_path)
visit new_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
- expect(find('h1.group-title')).to have_content(subgroup.name)
+ expect(find('h1.home-panel-title')).to have_content(subgroup.name)
end
it 'the old subgroup path redirects to the new path' do
update_path(new_group_path)
visit old_subgroup_full_path
expect(current_path).to eq(new_subgroup_full_path)
- expect(find('h1.group-title')).to have_content(subgroup.name)
+ expect(find('h1.home-panel-title')).to have_content(subgroup.name)
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index f3e573ccbc4..c2f32c76422 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -203,7 +203,7 @@ describe 'Group' do
visit path
- expect(page).to have_css('.group-home-desc > p > strong')
+ expect(page).to have_css('.home-panel-description-markdown > p > strong')
end
it 'passes through html-pipeline' do
@@ -211,7 +211,7 @@ describe 'Group' do
visit path
- expect(page).to have_css('.group-home-desc > p > gl-emoji')
+ expect(page).to have_css('.home-panel-description-markdown > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
@@ -219,7 +219,7 @@ describe 'Group' do
visit path
- expect(page).not_to have_css('.group-home-desc h1')
+ expect(page).not_to have_css('.home-panel-description-markdown h1')
end
it 'permits `rel` attribute on links' do
@@ -227,7 +227,7 @@ describe 'Group' do
visit path
- expect(page).to have_css('.group-home-desc a[rel]')
+ expect(page).to have_css('.home-panel-description-markdown a[rel]')
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index e910fb54d23..e0b1e286dee 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -37,7 +37,7 @@ describe 'Dropdown assignee', :js do
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
end
@@ -160,7 +160,7 @@ describe 'Dropdown assignee', :js 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_tokens([assignee_token('None')])
expect_filtered_search_input_empty
end
@@ -168,7 +168,7 @@ describe 'Dropdown assignee', :js 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_tokens([assignee_token('Any')])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 50d819a6161..bedc61b9eed 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -45,7 +45,7 @@ describe 'Dropdown author', :js do
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
expect(page).to have_css(js_dropdown_author, visible: false)
end
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index 97dd0afd002..f36d4e8f23f 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -64,7 +64,7 @@ describe 'Dropdown emoji', :js do
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
expect(page).to have_css(js_dropdown_emoji, visible: false)
end
@@ -125,7 +125,7 @@ describe 'Dropdown emoji', :js 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_tokens([reaction_token('None', false)])
expect_filtered_search_input_empty
end
@@ -133,7 +133,7 @@ describe 'Dropdown emoji', :js 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_tokens([reaction_token('Any', false)])
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index b25b1514d62..f502061dfce 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -238,7 +238,7 @@ describe 'Dropdown label', :js do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'None').click
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([label_token('none', false)])
+ expect_tokens([label_token('None', false)])
expect_filtered_search_input_empty
end
@@ -246,7 +246,7 @@ describe 'Dropdown label', :js do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'Any').click
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([label_token('any', false)])
+ expect_tokens([label_token('Any', false)])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index ef5801e61e8..b330eafe1d1 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -44,7 +44,7 @@ describe 'Dropdown milestone', :js do
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
expect(page).to have_css(js_dropdown_milestone, visible: false)
end
@@ -192,7 +192,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('None')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([milestone_token('none', false)])
+ expect_tokens([milestone_token('None', false)])
expect_filtered_search_input_empty
end
@@ -200,7 +200,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('Any')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([milestone_token('any', false)])
+ expect_tokens([milestone_token('Any', false)])
expect_filtered_search_input_empty
end
@@ -208,7 +208,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([milestone_token('upcoming', false)])
+ expect_tokens([milestone_token('Upcoming', false)])
expect_filtered_search_input_empty
end
@@ -216,7 +216,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('Started')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([milestone_token('started', false)])
+ expect_tokens([milestone_token('Started', false)])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index a29380a180e..fa8e5cb0ca9 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -108,7 +108,7 @@ describe 'Filter issues', :js do
it 'filters issues by no assignee' do
input_filtered_search('assignee:none')
- expect_tokens([assignee_token('none')])
+ expect_tokens([assignee_token('None')])
expect_issues_list_count(3)
expect_filtered_search_input_empty
end
@@ -146,7 +146,7 @@ describe 'Filter issues', :js do
it 'filters issues by no label' do
input_filtered_search('label:none')
- expect_tokens([label_token('none', false)])
+ expect_tokens([label_token('None', false)])
expect_issues_list_count(4)
expect_filtered_search_input_empty
end
@@ -287,7 +287,7 @@ describe 'Filter issues', :js do
it 'filters issues by no milestone' do
input_filtered_search("milestone:none")
- expect_tokens([milestone_token('none', false)])
+ expect_tokens([milestone_token('None', false)])
expect_issues_list_count(3)
expect_filtered_search_input_empty
end
@@ -299,7 +299,7 @@ describe 'Filter issues', :js do
input_filtered_search("milestone:upcoming")
- expect_tokens([milestone_token('upcoming', false)])
+ expect_tokens([milestone_token('Upcoming', false)])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
@@ -307,7 +307,7 @@ describe 'Filter issues', :js do
it 'filters issues by started milestones' do
input_filtered_search("milestone:started")
- expect_tokens([milestone_token('started', false)])
+ expect_tokens([milestone_token('Started', false)])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 1e1dd5691ab..a4c34ce85f0 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -122,7 +122,7 @@ describe 'Visual tokens', :js do
end
it 'changes value in visual token' do
- expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('None')
end
it 'moves input to the right' do
@@ -147,7 +147,7 @@ describe 'Visual tokens', :js do
it 'selects static option from dropdown' do
find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
- expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('Upcoming')
expect(is_input_focused).to eq(true)
end
@@ -348,7 +348,7 @@ describe 'Visual tokens', :js do
it 'tokenizes the search term to complete visual token' do
expect_tokens([
author_token(user.name),
- assignee_token('none')
+ assignee_token('None')
])
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 3b7a17ef355..c22ad0d20ef 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -279,7 +279,7 @@ describe 'GFM autocomplete', :js do
end
# This context has jsut one example in each contexts in order to improve spec performance.
- context 'labels' do
+ context 'labels', :quarantine do
let!(:backend) { create(:label, project: project, title: 'backend') }
let!(:bug) { create(:label, project: project, title: 'bug') }
let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') }
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 05228e27963..16754035076 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -19,9 +19,9 @@ describe 'Copy as GFM', :js do
visit project_issue_path(@project, @feat.issue)
end
- # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM.
- # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+ # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform GitLab Flavored Markdown (GFM) to HTML.
+ # The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js consequently transform that same HTML to GFM.
+ # To make sure these filters and nodes/marks are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
# These are all in a single `it` for performance reasons.
@@ -35,12 +35,15 @@ describe 'Copy as GFM', :js do
verify(
'a real world example from the gitlab-ce README',
- <<-GFM.strip_heredoc
+ <<~GFM
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Canonical source
@@ -51,27 +54,31 @@ describe 'Copy as GFM', :js do
To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
- - Manage Git repositories with fine grained access controls that keep your code secure
+ * Manage Git repositories with fine grained access controls that keep your code secure
- - Perform code reviews and enhance collaboration with merge requests
+ * Perform code reviews and enhance collaboration with merge requests
- - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+ * Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- - Each project can also have an issue tracker, issue board, and a wiki
+ * Each project can also have an issue tracker, issue board, and a wiki
- - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+ * Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- - Completely free and open source (MIT Expat license)
+ * Completely free and open source (MIT Expat license)
GFM
)
aggregate_failures('an accidentally selected empty element') do
gfm = '# Heading1'
- html = <<-HTML.strip_heredoc
+ html = <<~HTML
<h1>Heading1</h1>
<h2></h2>
+
+ <blockquote></blockquote>
+
+ <pre class="code highlight"></pre>
HTML
output_gfm = html_to_gfm(html)
@@ -81,7 +88,7 @@ describe 'Copy as GFM', :js do
aggregate_failures('an accidentally selected other element') do
gfm = 'Test comment with **Markdown!**'
- html = <<-HTML.strip_heredoc
+ html = <<~HTML
<li class="note">
<div class="md">
<p>
@@ -107,10 +114,17 @@ describe 'Copy as GFM', :js do
verify(
'TaskListFilter',
- '- [ ] Unchecked task',
- '- [x] Checked task',
- '1. [ ] Unchecked numbered task',
- '1. [x] Checked numbered task'
+ <<~GFM,
+ * [ ] Unchecked task
+
+ * [x] Checked task
+ GFM
+
+ <<~GFM
+ 1. [ ] Unchecked ordered task
+
+ 1. [x] Checked ordered task
+ GFM
)
verify(
@@ -139,7 +153,16 @@ describe 'Copy as GFM', :js do
verify(
'TableOfContentsFilter',
- '[[_TOC_]]'
+ <<~GFM,
+ [[_TOC_]]
+
+ # Heading 1
+
+ ## Heading 2
+ GFM
+
+ pipeline: :wiki,
+ project_wiki: @project.wiki
)
verify(
@@ -166,7 +189,7 @@ describe 'Copy as GFM', :js do
'$`c = \pm\sqrt{a^2 + b^2}`$',
# math block
- <<-GFM.strip_heredoc
+ <<~GFM
```math
c = \pm\sqrt{a^2 + b^2}
```
@@ -176,7 +199,7 @@ describe 'Copy as GFM', :js do
aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
- html = <<-HTML.strip_heredoc
+ html = <<~HTML
<span class="katex">
<span class="katex-mathml">
<math>
@@ -287,7 +310,7 @@ describe 'Copy as GFM', :js do
verify(
'MermaidFilter: mermaid as converted from GFM to HTML',
- <<-GFM.strip_heredoc
+ <<~GFM
```mermaid
graph TD;
A-->B;
@@ -296,14 +319,14 @@ describe 'Copy as GFM', :js do
)
aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do
- gfm = <<-GFM.strip_heredoc
+ gfm = <<~GFM
```mermaid
graph TD;
A-->B;
```
GFM
- html = <<-HTML.strip_heredoc
+ html = <<~HTML
<svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid">
<style>
.mermaid {
@@ -371,8 +394,7 @@ describe 'Copy as GFM', :js do
</g>
</g>
<text class="source" display="none">graph TD;
- A--&gt;B;
- </text>
+ A--&gt;B;</text>
</svg>
HTML
@@ -381,13 +403,82 @@ describe 'Copy as GFM', :js do
end
verify(
+ 'SuggestionFilter: suggestion as converted from GFM to HTML',
+
+ <<~GFM
+ ```suggestion
+ New
+ And newer
+ ```
+ GFM
+ )
+
+ aggregate_failures('SuggestionFilter: suggestion as transformed from HTML to Vue component') do
+ gfm = <<~GFM
+ ```suggestion
+ New
+ And newer
+ ```
+ GFM
+
+ html = <<~HTML
+ <div class="md-suggestion">
+ <div class="md-suggestion-header border-bottom-0 mt-2 qa-suggestion-diff-header">
+ <div class="qa-suggestion-diff-header font-weight-bold">
+ Suggested change
+ <a href="/gitlab/help/user/discussions/index.md#suggest-changes" aria-label="Help" class="js-help-btn">
+ <svg aria-hidden="true" class="s16 ic-question-o link-highlight">
+ <use xlink:href="/gitlab/assets/icons.svg#question-o"></use>
+ </svg>
+ </a>
+ </div>
+ <!---->
+ <button type="button" class="btn qa-apply-btn">Apply suggestion</button>
+ </div>
+ <table class="mb-3 md-suggestion-diff js-syntax-highlight code white">
+ <tbody>
+ <tr class="line_holder old">
+ <td class="diff-line-num old_line qa-old-diff-line-number old">9</td>
+ <td class="diff-line-num new_line old"></td>
+ <td class="line_content old"><span>Old
+ </span></td>
+ </tr>
+ <tr class="line_holder new">
+ <td class="diff-line-num old_line new"></td>
+ <td class="diff-line-num new_line qa-new-diff-line-number new">9</td>
+ <td class="line_content new"><span>New
+ </span></td>
+ </tr>
+ <tr class="line_holder new">
+ <td class="diff-line-num old_line new"></td>
+ <td class="diff-line-num new_line qa-new-diff-line-number new">10</td>
+ <td class="line_content new"><span> And newer
+ </span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ HTML
+
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+
+ verify(
'SanitizationFilter',
- <<-GFM.strip_heredoc
+ <<~GFM
<sub>sub</sub>
<dl>
<dt>dt</dt>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ <dd>dd</dd>
+
+ <dt>dt</dt>
+ <dt>dt</dt>
+ <dd>dd</dd>
<dd>dd</dd>
</dl>
@@ -399,30 +490,26 @@ describe 'Copy as GFM', :js do
<var>var</var>
- <ruby>ruby</ruby>
-
- <rt>rt</rt>
-
- <rp>rp</rp>
+ <abbr title="HyperText &quot;Markup&quot; Language">HTML</abbr>
- <abbr>abbr</abbr>
+ <details>
+ <summary>summary></summary>
- <summary>summary</summary>
-
- <details>details</details>
+ details
+ </details>
GFM
)
verify(
'SanitizationFilter',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```
Plain text
```
GFM
- <<-GFM.strip_heredoc,
+ <<~GFM,
```ruby
def foo
bar
@@ -430,11 +517,9 @@ describe 'Copy as GFM', :js do
```
GFM
- <<-GFM.strip_heredoc
+ <<~GFM
Foo
- This is an example of GFM
-
```js
Code goes here
```
@@ -452,9 +537,8 @@ describe 'Copy as GFM', :js do
'> Quote',
# multiline quote
- <<-GFM.strip_heredoc,
- > Multiline
- > Quote
+ <<~GFM,
+ > Multiline Quote
>
> With multiple paragraphs
GFM
@@ -465,48 +549,58 @@ describe 'Copy as GFM', :js do
'[Link](https://example.com)',
- '- List item',
+ <<~GFM,
+ * List item
+
+ * List item 2
+ GFM
# multiline list item
- <<-GFM.strip_heredoc,
- - Multiline
- List item
+ <<~GFM,
+ * Multiline
+
+ List item
GFM
# nested lists
- <<-GFM.strip_heredoc,
- - Nested
+ <<~GFM,
+ * Nested
- - Lists
+ * Lists
GFM
# list with blockquote
- <<-GFM.strip_heredoc,
- - List
+ <<~GFM,
+ * List
- > Blockquote
+ > Blockquote
GFM
- '1. Numbered list item',
+ <<~GFM,
+ 1. Ordered list item
+
+ 1. Ordered list item 2
+ GFM
- # multiline numbered list item
- <<-GFM.strip_heredoc,
+ # multiline ordered list item
+ <<~GFM,
1. Multiline
- Numbered list item
+
+ Ordered list item
GFM
- # nested numbered list
- <<-GFM.strip_heredoc,
+ # nested ordered list
+ <<~GFM,
1. Nested
- 1. Numbered lists
+ 1. Ordered lists
GFM
# list item followed by an HR
- <<-GFM.strip_heredoc,
- - list item
+ <<~GFM,
+ * list item
- -----
+ ---
GFM
'# Heading',
@@ -518,14 +612,14 @@ describe 'Copy as GFM', :js do
'**Bold**',
- '_Italics_',
+ '*Italics*',
'~~Strikethrough~~',
- '-----',
+ '---',
# table
- <<-GFM.strip_heredoc,
+ <<~GFM,
| Centered | Right | Left |
|:--------:|------:|------|
| Foo | Bar | **Baz** |
@@ -533,9 +627,9 @@ describe 'Copy as GFM', :js do
GFM
# table with empty heading
- <<-GFM.strip_heredoc,
+ <<~GFM,
| | x | y |
- |---|---|---|
+ |--|---|---|
| a | 1 | 0 |
| b | 0 | 1 |
GFM
@@ -545,9 +639,11 @@ describe 'Copy as GFM', :js do
alias_method :gfm_to_html, :markdown
def verify(label, *gfms)
+ markdown_options = gfms.extract_options!
+
aggregate_failures(label) do
gfms.each do |gfm|
- html = gfm_to_html(gfm).gsub(/\A&#x000A;|&#x000A;\z/, '')
+ html = gfm_to_html(gfm, markdown_options).gsub(/\A&#x000A;|&#x000A;\z/, '')
output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip)
end
@@ -594,7 +690,7 @@ describe 'Copy as GFM', :js do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```ruby
raise RuntimeError, "System commands must be given as an array of strings"
end
@@ -627,7 +723,7 @@ describe 'Copy as GFM', :js do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```ruby
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
@@ -645,7 +741,7 @@ describe 'Copy as GFM', :js do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```ruby
unless cmd.is_a?(Array)
raise RuntimeError, "System commands must be given as an array of strings"
@@ -691,7 +787,7 @@ describe 'Copy as GFM', :js do
verify(
'.line[id="LC9"], .line[id="LC10"]',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```ruby
raise RuntimeError, "System commands must be given as an array of strings"
end
@@ -733,7 +829,7 @@ describe 'Copy as GFM', :js do
verify(
'.line[id="LC27"], .line[id="LC28"]',
- <<-GFM.strip_heredoc,
+ <<~GFM,
```json
"bio": null,
"skype": "",
@@ -752,7 +848,7 @@ describe 'Copy as GFM', :js do
end
def html_for_selector(selector)
- js = <<-JS.strip_heredoc
+ js = <<~JS
(function(selector) {
var els = document.querySelectorAll(selector);
var htmls = [].slice.call(els).map(function(el) { return el.outerHTML; });
@@ -763,7 +859,7 @@ describe 'Copy as GFM', :js do
end
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
- js = <<-JS.strip_heredoc
+ js = <<~JS
(function(html) {
var transformer = window.CopyAsGFM[#{transformer.inspect}];
diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb
index 678ce80b382..16ad0d456be 100644
--- a/spec/features/markdown/math_spec.rb
+++ b/spec/features/markdown/math_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Math rendering', :js do
+ let!(:project) { create(:project, :public) }
+
it 'renders inline and display math correctly' do
description = <<~MATH
This math is inline $`a^2+b^2=c^2`$.
@@ -11,7 +13,6 @@ describe 'Math rendering', :js do
```
MATH
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -19,4 +20,19 @@ describe 'Math rendering', :js do
expect(page).to have_selector('.katex .mord.mathdefault', text: 'b')
expect(page).to have_selector('.katex-display .mord.mathdefault', text: 'b')
end
+
+ it 'only renders non XSS links' do
+ description = <<~MATH
+ This link is valid $`\\href{javascript:alert('xss');}{xss}`$.
+
+ This link is valid $`\\href{https://gitlab.com}{Gitlab}`$.
+ MATH
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_selector('.katex-error', text: "\href{javascript:alert('xss');}{xss}")
+ expect(page).to have_selector('.katex-html a', text: 'Gitlab')
+ end
end
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 00cf368e8c9..eb4b2cf5bd0 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -91,6 +91,7 @@ describe 'User comments on a diff', :js do
# Check the same comments in the side-by-side view.
execute_script("window.scrollTo(0,0);")
+ find('.js-show-diff-settings').click
click_button 'Side-by-side'
wait_for_requests
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index ba4806821f9..08fa4a98feb 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -126,6 +126,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
describe 'side-by-side view' do
before do
page.within('.merge-request-tabs') { click_link 'Changes' }
+ find('.js-show-diff-settings').click
page.find('#parallel-diff-btn').click
end
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index 4ab9a87ad4b..57be1d06708 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -88,5 +88,17 @@ describe 'Merge request > User sees discussions', :js do
expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}"
end
end
+
+ context 'a commit non-diff discussion' do
+ let(:note) { create(:discussion_note_on_commit, project: project) }
+
+ it 'displays correct header' do
+ page.within(find("#note_#{note.id}", match: :first)) do
+ refresh # Trigger a refresh of notes.
+ wait_for_requests
+ expect(page).to have_content "commented on commit #{note.commit_id[0...7]}"
+ end
+ end
+ end
end
end
diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
index dd860382daa..0decdfe3a14 100644
--- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
+++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
@@ -9,17 +9,23 @@ describe 'Merge request > User toggles whitespace changes', :js do
project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
+
+ find('.js-show-diff-settings').click
end
it 'has a button to toggle whitespace changes' do
- expect(page).to have_content 'Hide whitespace changes'
+ expect(page).to have_content 'Show whitespace changes'
end
describe 'clicking "Hide whitespace changes" button' do
it 'toggles the "Hide whitespace changes" button' do
- click_link 'Hide whitespace changes'
+ find('#show-whitespace').click
+
+ visit diffs_project_merge_request_path(project, merge_request)
+
+ find('.js-show-diff-settings').click
- expect(page).to have_content 'Show whitespace changes'
+ expect(find('#show-whitespace')).to be_checked
end
end
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 7f95a1282f9..0434db04113 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -23,6 +23,8 @@ describe 'User views diffs', :js do
end
it 'shows diffs' do
+ find('.js-show-diff-settings').click
+
expect(page).to have_css('.tab-content #diffs.active')
expect(page).to have_css('#parallel-diff-btn', count: 1)
expect(page).to have_css('#inline-diff-btn', count: 1)
@@ -38,6 +40,8 @@ describe 'User views diffs', :js do
context 'when in the side-by-side view' do
before do
+ find('.js-show-diff-settings').click
+
click_button 'Side-by-side'
wait_for_requests
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index e12532e97fa..1fa9babaff5 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do
page.within(find('.deploy-keys')) do
expect(page).to have_selector('.deploy-key', count: 1)
- accept_confirm { find('.ic-remove').click() }
+ accept_confirm { find('.ic-remove').click }
wait_for_requests
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 5de0bc009fb..fa785ed10ef 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -50,7 +50,7 @@ end
def check_content_reverted(template_content)
find('.template-selectors-undo-menu .btn-info').click
expect(page).not_to have_content(template_content)
- expect(find('.template-type-selector .dropdown-toggle-text')).to have_content()
+ expect(find('.template-type-selector .dropdown-toggle-text')).to have_content
end
def select_file_template(template_selector_selector, template_name)
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 8230396a4cc..24830b2bd3e 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -103,7 +103,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'shows commit`s data', :js do
- requests = inspect_requests() do
+ requests = inspect_requests do
visit project_job_path(project, job)
end
@@ -214,7 +214,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'downloads the zip file when user clicks the download button' do
- requests = inspect_requests() do
+ requests = inspect_requests do
click_link 'Download'
end
@@ -824,7 +824,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
before do
job.run!
visit project_job_path(project, job)
- find('.js-cancel-job').click()
+ find('.js-cancel-job').click
end
it 'loads the page and shows all needed controls' do
@@ -884,7 +884,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it do
- requests = inspect_requests() do
+ requests = inspect_requests do
visit download_project_job_artifacts_path(project, job2)
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 3192c9ffad4..72ef460d315 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -286,6 +286,49 @@ describe 'Pipeline', :js do
end
end
+ context 'when a bridge job exists' do
+ include_context 'pipeline builds'
+
+ let(:project) { create(:project, :repository) }
+ let(:downstream) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ ref: 'master',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ let!(:bridge) do
+ create(:ci_bridge, pipeline: pipeline,
+ name: 'cross-build',
+ user: user,
+ downstream: downstream)
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows the pipeline with a bridge job' do
+ expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_content('cross-build')
+ end
+ end
+
+ describe 'GET /:project/pipelines/:id/builds' do
+ before do
+ visit builds_project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows a bridge job on a list' do
+ expect(page).to have_content('cross-build')
+ expect(page).to have_content(bridge.id)
+ end
+ end
+ end
+
describe 'GET /:project/pipelines/:id/builds' do
include_context 'pipeline builds'
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 1982136b89d..1259ad45791 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click
fill_in 'deploy_key_title', with: 'updated_deploy_key'
check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
@@ -71,14 +71,14 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project)
- find('.js-deployKeys-tab-available_project_keys').click()
+ find('.js-deployKeys-tab-available_project_keys').click
- find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click
fill_in 'deploy_key_title', with: 'updated_deploy_key'
click_button 'Save changes'
- find('.js-deployKeys-tab-available_project_keys').click()
+ find('.js-deployKeys-tab-available_project_keys').click
expect(page).to have_content('updated_deploy_key')
end
@@ -87,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() }
+ accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click }
expect(page).not_to have_content(private_deploy_key.title)
end
diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb
index fcf05e04a5c..7dc18601f50 100644
--- a/spec/features/projects/settings/user_changes_default_branch_spec.rb
+++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb
@@ -15,6 +15,9 @@ describe 'Projects > Settings > User changes default branch' do
let(:project) { create(:project, :repository, namespace: user.namespace) }
it 'allows to change the default branch', :js do
+ # Otherwise, running JS may overwrite our change to project_default_branch
+ wait_for_requests
+
select2('fix', from: '#project_default_branch')
page.within '#default-branch-settings' do
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
index d82e350e0f7..9c1ef78b0ca 100644
--- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -31,7 +31,7 @@ describe 'Projects > Snippets > User comments on a snippet', :js do
end
it 'should have zen mode' do
- find('.js-zen-enter').click()
+ find('.js-zen-enter').click
expect(page).to have_selector('.fullscreen')
end
end
diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb
new file mode 100644
index 00000000000..ebb2844d17f
--- /dev/null
+++ b/spec/features/projects/tags/user_edits_tags_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Project > Tags', :js do
+ include DropzoneHelper
+
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ sign_in(user)
+ project.add_role(user, role)
+ end
+
+ describe 'when opening project tags' do
+ before do
+ visit project_tags_path(project)
+ end
+
+ context 'page with tags list' do
+ it 'shows tag name' do
+ page.within first('.tags > .content-list > li') do
+ expect(page.find('.row-main-content')).to have_content 'v1.1.0 Version 1.1.0'
+ end
+ end
+
+ it 'shows tag edit button' do
+ page.within first('.tags > .content-list > li') do
+ edit_btn = page.find('.row-fixed-content.controls a.btn-edit')
+
+ expect(edit_btn['href']).to have_content '/tags/v1.1.0/release/edit'
+ end
+ end
+ end
+
+ context 'edit tag release notes' do
+ before do
+ find('.tags > .content-list > li:first-child .row-fixed-content.controls a.btn-edit').click
+ end
+
+ it 'shows tag name header' do
+ page.within('.content') do
+ expect(page.find('.sub-header-block')).to have_content 'Release notes for tag v1.1.0'
+ end
+ end
+
+ it 'shows release notes form' do
+ page.within('.content') do
+ expect(page).to have_selector('form.release-form')
+ end
+ end
+
+ it 'toolbar buttons on release notes form are functional' do
+ page.within('.content form.release-form') do
+ note_textarea = page.find('.js-gfm-input')
+
+ # Click on Bold button
+ page.find('.md-header-toolbar button.toolbar-btn:first-child').click
+
+ expect(note_textarea.value).to eq('****')
+ end
+ end
+
+ it 'release notes form shows "Attach a file" button', :js do
+ page.within('.content form.release-form') do
+ expect(page).to have_button('Attach a file')
+ expect(page).not_to have_selector('.uploading-progress-container', visible: true)
+ end
+ end
+
+ it 'shows "Attaching a file" message on uploading 1 file', :js do
+ slow_requests do
+ dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
+
+ expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 4cb49ab02e2..f7efc3f325c 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -55,30 +55,30 @@ describe 'Project' do
it 'parses Markdown' do
project.update_attribute(:description, 'This is **my** project')
visit path
- expect(page).to have_css('.project-description > .project-description-markdown > p > strong')
+ expect(page).to have_css('.home-panel-description > .home-panel-description-markdown > p > strong')
end
it 'passes through html-pipeline' do
project.update_attribute(:description, 'This project is the :poop:')
visit path
- expect(page).to have_css('.project-description > .project-description-markdown > p > gl-emoji')
+ expect(page).to have_css('.home-panel-description > .home-panel-description-markdown > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
project.update_attribute(:description, "```\ncode\n```")
visit path
- expect(page).not_to have_css('.project-description code')
+ expect(page).not_to have_css('.home-panel-description code')
end
it 'permits `rel` attribute on links' do
project.update_attribute(:description, 'https://google.com/')
visit path
- expect(page).to have_css('.project-description a[rel]')
+ expect(page).to have_css('.home-panel-description a[rel]')
end
context 'read more', :js do
let(:read_more_selector) { '.read-more-container' }
- let(:read_more_trigger_selector) { '.project-home-desc .js-read-more-trigger' }
+ let(:read_more_trigger_selector) { '.home-panel-home-desc .js-read-more-trigger' }
it 'does not display "read more" link on desktop breakpoint' do
project.update_attribute(:description, 'This is **my** project')
@@ -94,7 +94,7 @@ describe 'Project' do
find(read_more_trigger_selector).click
- expect(page).to have_css('.project-description .is-expanded')
+ expect(page).to have_css('.home-panel-description .is-expanded')
end
end
end
@@ -111,14 +111,14 @@ describe 'Project' do
it 'shows project topics' do
project.update_attribute(:tag_list, 'topic1')
visit path
- expect(page).to have_css('.project-topic-list')
+ expect(page).to have_css('.home-panel-topic-list')
expect(page).to have_content('topic1')
end
it 'shows up to 3 project tags' do
project.update_attribute(:tag_list, 'topic1, topic2, topic3, topic4')
visit path
- expect(page).to have_css('.project-topic-list')
+ expect(page).to have_css('.home-panel-topic-list')
expect(page).to have_content('topic1, topic2, topic3 + 1 more')
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 843dbcd5b4d..e23000fa676 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -452,9 +452,9 @@ describe "Internal Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_allowed_for(:guest).of(project) }
- it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index cf0837c1e67..f380bc122a7 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -485,7 +485,7 @@ describe "Private Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 7e1b735fd3d..57d56371719 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -272,11 +272,11 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_allowed_for(:guest).of(project) }
- it { is_expected.to be_allowed_for(:user) }
- it { is_expected.to be_allowed_for(:external) }
- it { is_expected.to be_allowed_for(:visitor) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/environments" do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 81fb4e3561c..ee84fd067d4 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -31,4 +31,16 @@ describe ContributedProjectsFinder do
it { is_expected.to match_array([private_project, internal_project, public_project]) }
end
+
+ context 'user with private profile' do
+ it 'does not return contributed projects' do
+ private_user = create(:user, private_profile: true)
+ public_project.add_maintainer(private_user)
+ create(:push_event, project: public_project, author: private_user)
+
+ projects = described_class.new(private_user).execute(current_user)
+
+ expect(projects).to be_empty
+ end
+ end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index ff4c6b8dd42..107da08a0a9 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -68,20 +68,34 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(2)
end
- it 'filters by group' do
- params = { group_id: group.id }
+ context 'filtering by group' do
+ it 'includes all merge requests when user has access' do
+ params = { group_id: group.id }
- merge_requests = described_class.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(3)
- end
+ expect(merge_requests.size).to eq(3)
+ end
- it 'filters by group including subgroups', :nested_groups do
- params = { group_id: group.id, include_subgroups: true }
+ it 'excludes merge requests from projects the user does not have access to' do
+ private_project = create_project_without_n_plus_1(:private, group: group)
+ private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
+ params = { group_id: group.id }
- merge_requests = described_class.new(user, params).execute
+ private_project.add_guest(user)
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(6)
+ expect(merge_requests.size).to eq(3)
+ expect(merge_requests).not_to include(private_mr)
+ end
+
+ it 'filters by group including subgroups', :nested_groups do
+ params = { group_id: group.id, include_subgroups: true }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests.size).to eq(6)
+ end
end
it 'filters by non_archived' do
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index 656d120311a..ecffbb9e197 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -69,6 +69,12 @@ describe MilestonesFinder do
expect(result.to_a).to contain_exactly(milestone_1)
end
+
+ it 'filters by search_title' do
+ result = described_class.new(params.merge(search_title: 'one t')).execute
+
+ expect(result.to_a).to contain_exactly(milestone_1)
+ end
end
describe '#find_by' do
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 3d9e0628f63..138a6c5ed6b 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -30,6 +30,7 @@
]
}
},
+ "version": { "type": "string" },
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
diff --git a/spec/fixtures/api/schemas/error_tracking/list_projects.json b/spec/fixtures/api/schemas/error_tracking/list_projects.json
new file mode 100644
index 00000000000..2aaa525e38f
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/list_projects.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": [
+ "projects"
+ ],
+ "properties": {
+ "projects": {
+ "type": "array",
+ "items": { "$ref": "project.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/error_tracking/project.json b/spec/fixtures/api/schemas/error_tracking/project.json
new file mode 100644
index 00000000000..f6d611133c7
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/project.json
@@ -0,0 +1,19 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "slug",
+ "organization_slug",
+ "name"
+ ],
+ "properties" : {
+ "id": { "type": "string"},
+ "name": { "type": "string" },
+ "slug": { "type": "string" },
+ "status": { "type": "string" },
+ "organization_name": { "type": "string" },
+ "organization_slug": { "type": "string" },
+ "organization_id": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json
index 4175642eb00..e0fd4620c43 100644
--- a/spec/fixtures/api/schemas/registry/repository.json
+++ b/spec/fixtures/api/schemas/registry/repository.json
@@ -2,20 +2,27 @@
"type": "object",
"required" : [
"id",
+ "name",
"path",
"location",
- "tags_path"
+ "created_at"
],
"properties" : {
"id": {
"type": "integer"
},
+ "name": {
+ "type": "string"
+ },
"path": {
"type": "string"
},
"location": {
"type": "string"
},
+ "created_at": {
+ "type": "date-time"
+ },
"tags_path": {
"type": "string"
},
diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json
index 3a2c88791e1..48f8402b65b 100644
--- a/spec/fixtures/api/schemas/registry/tag.json
+++ b/spec/fixtures/api/schemas/registry/tag.json
@@ -2,15 +2,22 @@
"type": "object",
"required" : [
"name",
+ "path",
"location"
],
"properties" : {
"name": {
"type": "string"
},
+ "path": {
+ "type": "string"
+ },
"location": {
"type": "string"
},
+ "digest": {
+ "type": "string"
+ },
"revision": {
"type": "string"
},
diff --git a/spec/fixtures/pages_non_writeable.zip b/spec/fixtures/pages_non_writeable.zip
new file mode 100644
index 00000000000..69f175d8504
--- /dev/null
+++ b/spec/fixtures/pages_non_writeable.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip b/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip
new file mode 100644
index 00000000000..b9ae1548713
--- /dev/null
+++ b/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/invalid-symlinks-outside.zip b/spec/fixtures/safe_zip/invalid-symlinks-outside.zip
new file mode 100644
index 00000000000..c184a1dafe2
--- /dev/null
+++ b/spec/fixtures/safe_zip/invalid-symlinks-outside.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/valid-non-writeable.zip b/spec/fixtures/safe_zip/valid-non-writeable.zip
new file mode 100644
index 00000000000..69f175d8504
--- /dev/null
+++ b/spec/fixtures/safe_zip/valid-non-writeable.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/valid-simple.zip b/spec/fixtures/safe_zip/valid-simple.zip
new file mode 100644
index 00000000000..a56b8b41dcc
--- /dev/null
+++ b/spec/fixtures/safe_zip/valid-simple.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/valid-symlinks-first.zip b/spec/fixtures/safe_zip/valid-symlinks-first.zip
new file mode 100644
index 00000000000..f5952ef71c9
--- /dev/null
+++ b/spec/fixtures/safe_zip/valid-symlinks-first.zip
Binary files differ
diff --git a/spec/fixtures/sentry/list_projects_sample_response.json b/spec/fixtures/sentry/list_projects_sample_response.json
new file mode 100644
index 00000000000..fd79b0d0f30
--- /dev/null
+++ b/spec/fixtures/sentry/list_projects_sample_response.json
@@ -0,0 +1,81 @@
+[
+ {
+ "status": "active",
+ "features": [
+ "data-forwarding",
+ "rate-limits",
+ "releases"
+ ],
+ "color": "#5c3fbf",
+ "isInternal": false,
+ "isPublic": false,
+ "dateCreated": "2018-12-11T10:41:22.476Z",
+ "id": "2",
+ "slug": "sentry-example",
+ "name": "sentry-example",
+ "hasAccess": true,
+ "isBookmarked": false,
+ "platform": "node",
+ "firstEvent": "2018-12-12T15:07:18Z",
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "isMember": true,
+ "organization": {
+ "status": {
+ "id": "active",
+ "name": "active"
+ },
+ "require2FA": false,
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "name": "Sentry",
+ "dateCreated": "2018-12-11T10:21:47.431Z",
+ "id": "1",
+ "isEarlyAdopter": false,
+ "slug": "sentry"
+ }
+ },
+ {
+ "status": "active",
+ "features": [
+ "data-forwarding",
+ "rate-limits"
+ ],
+ "color": "#bf873f",
+ "isInternal": true,
+ "isPublic": false,
+ "dateCreated": "2018-12-11T10:21:47.440Z",
+ "id": "1",
+ "slug": "internal",
+ "name": "Internal",
+ "hasAccess": true,
+ "isBookmarked": false,
+ "platform": null,
+ "firstEvent": "2018-12-11T10:54:35Z",
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "isMember": true,
+ "organization": {
+ "status": {
+ "id": "active",
+ "name": "active"
+ },
+ "require2FA": false,
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "name": "Sentry",
+ "dateCreated": "2018-12-11T10:21:47.431Z",
+ "id": "1",
+ "isEarlyAdopter": false,
+ "slug": "sentry"
+ }
+ }
+]
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 3820cf5cb9d..23d7e41803e 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -1,6 +1,20 @@
require 'spec_helper'
describe EmailsHelper do
+ describe 'sanitize_name' do
+ context 'when name contains a valid URL string' do
+ it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do
+ expect(sanitize_name('https://about.gitlab.com')).to eq('https://about_gitlab_com')
+ expect(sanitize_name('www.gitlab.com')).to eq('www_gitlab_com')
+ expect(sanitize_name('//about.gitlab.com/handbook/security/#best-practices')).to eq('//about_gitlab_com/handbook/security/#best-practices')
+ end
+
+ it 'returns name as it is when it does not contain a URL' do
+ expect(sanitize_name('Foo Bar')).to eq('Foo Bar')
+ end
+ end
+ end
+
describe 'password_reset_token_valid_time' do
def validate_time_string(time_limit, expected_string)
Devise.reset_password_within = time_limit
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index cb0ea4e26ba..af4931e3370 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -2,6 +2,10 @@ require 'rails_helper'
describe ImportHelper do
describe '#sanitize_project_name' do
+ it 'removes leading tildes' do
+ expect(helper.sanitize_project_name('~~root')).to eq('root')
+ end
+
it 'removes whitespace' do
expect(helper.sanitize_project_name('my test repo')).to eq('my-test-repo')
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 412ec910f3a..03e3a72a82f 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -173,6 +173,7 @@ describe IssuablesHelper do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(true)
+ stub_commonmark_sourcepos_disabled
end
it 'returns the correct json for an issue' do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 4590904c93d..908e8960f37 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -16,7 +16,7 @@ describe MembersHelper do
it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" }
it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" }
it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" }
- it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
+ it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group and any subresources?" }
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
@@ -33,7 +33,7 @@ describe MembersHelper do
it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
- it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
+ it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' }
it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 21461e46cf4..0715f34dafe 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -185,8 +185,8 @@ describe NotesHelper do
context 'for a non-diff discussion' do
let(:discussion) { create(:discussion_note_on_commit, project: project).to_discussion }
- it 'returns the commit path' do
- expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit))
+ it 'returns the commit path with the note anchor' do
+ expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: "note_#{discussion.first_note.id}"))
end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 88b5d87f087..10f61731206 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -354,8 +354,40 @@ describe ProjectsHelper do
allow(project).to receive(:builds_enabled?).and_return(false)
end
- it "do not include pipelines tab" do
- is_expected.not_to include(:pipelines)
+ context 'when user has access to builds' do
+ it "does include pipelines tab" do
+ is_expected.to include(:pipelines)
+ end
+ end
+
+ context 'when user does not have access to builds' do
+ before do
+ allow(helper).to receive(:can?) { false }
+ end
+
+ it "does not include pipelines tab" do
+ is_expected.not_to include(:pipelines)
+ end
+ end
+ end
+
+ context 'when project has external wiki' do
+ before do
+ allow(project).to receive(:has_external_wiki?).and_return(true)
+ end
+
+ it 'includes external wiki tab' do
+ is_expected.to include(:external_wiki)
+ end
+ end
+
+ context 'when project does not have external wiki' do
+ before do
+ allow(project).to receive(:has_external_wiki?).and_return(false)
+ end
+
+ it 'does not include external wiki tab' do
+ is_expected.not_to include(:external_wiki)
end
end
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 8662cadc7a0..ea48c69e0ae 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -6,7 +6,7 @@ describe SubmoduleHelper do
describe 'submodule links' do
let(:submodule_item) { double(id: 'hash', path: 'rack') }
let(:config) { Gitlab.config.gitlab }
- let(:repo) { double() }
+ let(:repo) { double }
before do
self.instance_variable_set(:@repository, repo)
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index cf8c1b77861..6179a02ce16 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -87,7 +87,7 @@ describe('CopyAsGFM', () => {
spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy();
- const expectedGFM = '- List Item1\n- List Item2';
+ const expectedGFM = '* List Item1\n\n* List Item2';
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
});
@@ -97,7 +97,7 @@ describe('CopyAsGFM', () => {
spyOn(window, 'getSelection').and.returnValue(selection);
simulateCopy();
- const expectedGFM = '1. List Item1\n1. List Item2';
+ const expectedGFM = '1. List Item1\n\n1. List Item2';
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
});
diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
index b709b937180..fe827bb1e18 100644
--- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -186,7 +186,7 @@ describe('ShortcutsIssuable', function() {
it('adds the quoted selection to the input', () => {
ShortcutsIssuable.replyWithSelectedText(true);
- expect($(FORM_SELECTOR).val()).toBe('> _Selected text._\n\n');
+ expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
});
it('triggers `focus`', () => {
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index a976c6b837f..2f0385454d7 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -51,15 +51,6 @@ describe('CompareVersions', () => {
});
});
- 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');
@@ -106,30 +97,6 @@ describe('CompareVersions', () => {
});
});
- 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);
- });
- });
-
describe('commit', () => {
beforeEach(done => {
vm.$store.state.diffs.commit = getDiffWithCommit().commit;
diff --git a/spec/javascripts/diffs/components/settings_dropdown_spec.js b/spec/javascripts/diffs/components/settings_dropdown_spec.js
new file mode 100644
index 00000000000..5031846cff0
--- /dev/null
+++ b/spec/javascripts/diffs/components/settings_dropdown_spec.js
@@ -0,0 +1,167 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import diffModule from '~/diffs/store/modules';
+import SettingsDropdown from '~/diffs/components/settings_dropdown.vue';
+import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+
+describe('Diff settiings dropdown component', () => {
+ let vm;
+ let actions;
+
+ function createComponent(extendStore = () => {}) {
+ const localVue = createLocalVue();
+
+ localVue.use(Vuex);
+
+ const store = new Vuex.Store({
+ modules: {
+ diffs: {
+ namespaced: true,
+ actions,
+ state: diffModule().state,
+ getters: diffModule().getters,
+ },
+ },
+ });
+
+ extendStore(store);
+
+ vm = mount(SettingsDropdown, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ actions = {
+ setInlineDiffViewType: jasmine.createSpy('setInlineDiffViewType'),
+ setParallelDiffViewType: jasmine.createSpy('setParallelDiffViewType'),
+ setRenderTreeList: jasmine.createSpy('setRenderTreeList'),
+ setShowWhitespace: jasmine.createSpy('setShowWhitespace'),
+ };
+ });
+
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ describe('tree view buttons', () => {
+ it('list view button dispatches setRenderTreeList with false', () => {
+ createComponent();
+
+ vm.find('.js-list-view').trigger('click');
+
+ expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), false, undefined);
+ });
+
+ it('tree view button dispatches setRenderTreeList with true', () => {
+ createComponent();
+
+ vm.find('.js-tree-view').trigger('click');
+
+ expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), true, undefined);
+ });
+
+ it('sets list button as active when renderTreeList is false', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ renderTreeList: false,
+ });
+ });
+
+ expect(vm.find('.js-list-view').classes('active')).toBe(true);
+ expect(vm.find('.js-tree-view').classes('active')).toBe(false);
+ });
+
+ it('sets tree button as active when renderTreeList is true', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ renderTreeList: true,
+ });
+ });
+
+ expect(vm.find('.js-list-view').classes('active')).toBe(false);
+ expect(vm.find('.js-tree-view').classes('active')).toBe(true);
+ });
+ });
+
+ describe('compare changes', () => {
+ it('sets inline button as active', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ diffViewType: INLINE_DIFF_VIEW_TYPE,
+ });
+ });
+
+ expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true);
+ expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false);
+ });
+
+ it('sets parallel button as active', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ diffViewType: PARALLEL_DIFF_VIEW_TYPE,
+ });
+ });
+
+ expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false);
+ expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true);
+ });
+
+ it('calls setInlineDiffViewType when clicking inline button', () => {
+ createComponent();
+
+ vm.find('.js-inline-diff-button').trigger('click');
+
+ expect(actions.setInlineDiffViewType).toHaveBeenCalled();
+ });
+
+ it('calls setParallelDiffViewType when clicking parallel button', () => {
+ createComponent();
+
+ vm.find('.js-parallel-diff-button').trigger('click');
+
+ expect(actions.setParallelDiffViewType).toHaveBeenCalled();
+ });
+ });
+
+ describe('whitespace toggle', () => {
+ it('does not set as checked when showWhitespace is false', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ showWhitespace: false,
+ });
+ });
+
+ expect(vm.find('#show-whitespace').element.checked).toBe(false);
+ });
+
+ it('sets as checked when showWhitespace is true', () => {
+ createComponent(store => {
+ Object.assign(store.state.diffs, {
+ showWhitespace: true,
+ });
+ });
+
+ expect(vm.find('#show-whitespace').element.checked).toBe(true);
+ });
+
+ it('calls setShowWhitespace on change', () => {
+ createComponent();
+
+ const checkbox = vm.find('#show-whitespace');
+
+ checkbox.element.checked = true;
+ checkbox.trigger('change');
+
+ expect(actions.setShowWhitespace).toHaveBeenCalledWith(
+ jasmine.anything(),
+ {
+ showWhitespace: true,
+ pushState: true,
+ },
+ undefined,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index 0a903bb7519..08b0b4f9e45 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -111,7 +111,7 @@ describe('Diffs tree list component', () => {
});
it('renders as file list when renderTreeList is false', done => {
- vm.renderTreeList = false;
+ vm.$store.state.diffs.renderTreeList = false;
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.file-row').length).toBe(1);
@@ -121,7 +121,7 @@ describe('Diffs tree list component', () => {
});
it('renders file paths when renderTreeList is false', done => {
- vm.renderTreeList = false;
+ vm.$store.state.diffs.renderTreeList = false;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.file-row').textContent).toContain('index.js');
@@ -129,34 +129,6 @@ describe('Diffs tree list component', () => {
done();
});
});
-
- it('hides render buttons when input is focused', done => {
- const focusEvent = new Event('focus');
-
- vm.$el.querySelector('.form-control').dispatchEvent(focusEvent);
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none');
-
- done();
- });
- });
-
- it('shows render buttons when input is blurred', done => {
- const blurEvent = new Event('blur');
- vm.focusSearch = true;
-
- vm.$nextTick()
- .then(() => {
- vm.$el.querySelector('.form-control').dispatchEvent(blurEvent);
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none');
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('clearSearch', () => {
@@ -168,24 +140,4 @@ describe('Diffs tree list component', () => {
expect(vm.search).toBe('');
});
});
-
- describe('toggleRenderTreeList', () => {
- it('updates renderTreeList', () => {
- expect(vm.renderTreeList).toBe(true);
-
- vm.toggleRenderTreeList(false);
-
- expect(vm.renderTreeList).toBe(false);
- });
- });
-
- describe('toggleFocusSearch', () => {
- it('updates focusSearch', () => {
- expect(vm.focusSearch).toBe(false);
-
- vm.toggleFocusSearch(true);
-
- expect(vm.focusSearch).toBe(true);
- });
- });
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 033b5e86dbe..b53ae4cecfd 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -27,6 +27,8 @@ import actions, {
scrollToFile,
toggleShowTreeList,
renderFileForDiscussionId,
+ setRenderTreeList,
+ setShowWhitespace,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -796,4 +798,55 @@ describe('DiffsStoreActions', () => {
expect(scrollToElement).not.toHaveBeenCalled();
});
});
+
+ describe('setRenderTreeList', () => {
+ it('commits SET_RENDER_TREE_LIST', done => {
+ testAction(
+ setRenderTreeList,
+ true,
+ {},
+ [{ type: types.SET_RENDER_TREE_LIST, payload: true }],
+ [],
+ done,
+ );
+ });
+
+ it('sets localStorage', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+
+ setRenderTreeList({ commit() {} }, true);
+
+ expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true);
+ });
+ });
+
+ describe('setShowWhitespace', () => {
+ it('commits SET_SHOW_WHITESPACE', done => {
+ testAction(
+ setShowWhitespace,
+ { showWhitespace: true },
+ {},
+ [{ type: types.SET_SHOW_WHITESPACE, payload: true }],
+ [],
+ done,
+ );
+ });
+
+ it('sets localStorage', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true });
+
+ expect(localStorage.setItem).toHaveBeenCalledWith('mr_show_whitespace', true);
+ });
+
+ it('calls history pushState', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(window.history.pushState).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index c595c38ef55..a6f3f9b9dc3 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -650,4 +650,28 @@ describe('DiffsStoreMutations', () => {
expect(state.tree).toEqual(['tree']);
});
});
+
+ describe('SET_RENDER_TREE_LIST', () => {
+ it('sets renderTreeList', () => {
+ const state = {
+ renderTreeList: true,
+ };
+
+ mutations[types.SET_RENDER_TREE_LIST](state, false);
+
+ expect(state.renderTreeList).toBe(false);
+ });
+ });
+
+ describe('SET_SHOW_WHITESPACE', () => {
+ it('sets showWhitespace', () => {
+ const state = {
+ showWhitespace: true,
+ };
+
+ mutations[types.SET_SHOW_WHITESPACE](state, false);
+
+ expect(state.showWhitespace).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index 3641946518b..c5e413a29d8 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -667,6 +667,47 @@ describe('DiffsStoreUtils', () => {
},
{
type: 'tree',
+ name: 'ee',
+ tree: [
+ {
+ type: 'tree',
+ name: 'lib',
+ tree: [
+ {
+ type: 'tree',
+ name: 'ee',
+ tree: [
+ {
+ type: 'tree',
+ name: 'gitlab',
+ tree: [
+ {
+ type: 'tree',
+ name: 'checks',
+ tree: [
+ {
+ type: 'tree',
+ name: 'longtreenametomakepath',
+ tree: [
+ {
+ type: 'blob',
+ name: 'diff_check.rb',
+ tree: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'tree',
name: 'spec',
tree: [
{
@@ -698,6 +739,17 @@ describe('DiffsStoreUtils', () => {
},
{
type: 'tree',
+ name: 'ee/lib/…/…/…/longtreenametomakepath',
+ tree: [
+ {
+ name: 'diff_check.rb',
+ tree: [],
+ type: 'blob',
+ },
+ ],
+ },
+ {
+ type: 'tree',
name: 'spec',
tree: [
{
diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js
index ab032b4cb98..bb8fb74c068 100644
--- a/spec/javascripts/ide/components/ide_status_bar_spec.js
+++ b/spec/javascripts/ide/components/ide_status_bar_spec.js
@@ -76,6 +76,9 @@ describe('ideStatusBar', () => {
icon: 'status_success',
},
},
+ commit: {
+ author_gravatar_url: 'www',
+ },
});
vm.$nextTick()
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index a112361e0d1..4118774cca3 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -56,7 +56,7 @@ describe('Multi-file editor library decorations controller', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.decorations.size).toBe(1);
- expect(controller.decorations.keys().next().value).toBe('path--path');
+ expect(controller.decorations.keys().next().value).toBe('gitlab:path--path');
});
it('calls decorate method', () => {
@@ -90,7 +90,7 @@ describe('Multi-file editor library decorations controller', () => {
controller.decorate(model);
- expect(controller.editorDecorations.keys().next().value).toBe('path--path');
+ expect(controller.editorDecorations.keys().next().value).toBe('gitlab:path--path');
});
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 121c4040212..e3fd9604474 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -680,51 +680,131 @@ describe('common_utils', () => {
});
});
- describe('deep: true', () => {
- it('converts object with child objects', () => {
- const obj = {
- snake_key: {
- child_snake_key: 'value',
- },
- };
-
- expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
- snakeKey: {
- childSnakeKey: 'value',
- },
- });
- });
+ describe('with options', () => {
+ const objWithoutChildren = {
+ project_name: 'GitLab CE',
+ group_name: 'GitLab.org',
+ license_type: 'MIT',
+ };
- it('converts array with child objects', () => {
- const arr = [
- {
- child_snake_key: 'value',
- },
- ];
-
- expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
- {
- childSnakeKey: 'value',
- },
- ]);
- });
+ const objWithChildren = {
+ project_name: 'GitLab CE',
+ group_name: 'GitLab.org',
+ license_type: 'MIT',
+ tech_stack: {
+ backend: 'Ruby',
+ frontend_framework: 'Vue',
+ database: 'PostgreSQL',
+ },
+ };
+
+ describe('when options.deep is true', () => {
+ it('converts object with child objects', () => {
+ const obj = {
+ snake_key: {
+ child_snake_key: 'value',
+ },
+ };
+
+ expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({
+ snakeKey: {
+ childSnakeKey: 'value',
+ },
+ });
+ });
- it('converts array with child arrays', () => {
- const arr = [
- [
+ it('converts array with child objects', () => {
+ const arr = [
{
child_snake_key: 'value',
},
- ],
- ];
+ ];
- expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
- [
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
{
childSnakeKey: 'value',
},
- ],
- ]);
+ ]);
+ });
+
+ it('converts array with child arrays', () => {
+ const arr = [
+ [
+ {
+ child_snake_key: 'value',
+ },
+ ],
+ ];
+
+ expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([
+ [
+ {
+ childSnakeKey: 'value',
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('when options.dropKeys is provided', () => {
+ it('discards properties mentioned in `dropKeys` array', () => {
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, {
+ dropKeys: ['group_name'],
+ }),
+ ).toEqual({
+ projectName: 'GitLab CE',
+ licenseType: 'MIT',
+ });
+ });
+
+ it('discards properties mentioned in `dropKeys` array when `deep` is true', () => {
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(objWithChildren, {
+ deep: true,
+ dropKeys: ['group_name', 'database'],
+ }),
+ ).toEqual({
+ projectName: 'GitLab CE',
+ licenseType: 'MIT',
+ techStack: {
+ backend: 'Ruby',
+ frontendFramework: 'Vue',
+ },
+ });
+ });
+ });
+
+ describe('when options.ignoreKeyNames is provided', () => {
+ it('leaves properties mentioned in `ignoreKeyNames` array intact', () => {
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, {
+ ignoreKeyNames: ['group_name'],
+ }),
+ ).toEqual({
+ projectName: 'GitLab CE',
+ licenseType: 'MIT',
+ group_name: 'GitLab.org',
+ });
+ });
+
+ it('leaves properties mentioned in `ignoreKeyNames` array intact when `deep` is true', () => {
+ expect(
+ commonUtils.convertObjectPropsToCamelCase(objWithChildren, {
+ deep: true,
+ ignoreKeyNames: ['group_name', 'frontend_framework'],
+ }),
+ ).toEqual({
+ projectName: 'GitLab CE',
+ group_name: 'GitLab.org',
+ licenseType: 'MIT',
+ techStack: {
+ backend: 'Ruby',
+ frontend_framework: 'Vue',
+ database: 'PostgreSQL',
+ },
+ });
+ });
});
});
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 18ad9843d22..b4e2cd75d47 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -6597,58 +6597,46 @@ export function convertDatesMultipleSeries(multipleSeries) {
export const environmentData = [
{
+ id: 34,
name: 'production',
- size: 1,
- latest: {
- id: 34,
- name: 'production',
- state: 'available',
- external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
- environment_type: null,
- stop_action: false,
- metrics_path: '/root/hello-prometheus/environments/34/metrics',
- environment_path: '/root/hello-prometheus/environments/34',
- stop_path: '/root/hello-prometheus/environments/34/stop',
- terminal_path: '/root/hello-prometheus/environments/34/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/production',
- created_at: '2018-06-29T16:53:38.301Z',
- updated_at: '2018-06-29T16:57:09.825Z',
- last_deployment: {
- id: 127,
- },
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
+ environment_type: null,
+ stop_action: false,
+ metrics_path: '/root/hello-prometheus/environments/34/metrics',
+ environment_path: '/root/hello-prometheus/environments/34',
+ stop_path: '/root/hello-prometheus/environments/34/stop',
+ terminal_path: '/root/hello-prometheus/environments/34/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/production',
+ created_at: '2018-06-29T16:53:38.301Z',
+ updated_at: '2018-06-29T16:57:09.825Z',
+ last_deployment: {
+ id: 127,
},
},
{
- name: 'review',
- size: 1,
- latest: {
- id: 35,
- name: 'review/noop-branch',
- state: 'available',
- external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
- environment_type: 'review',
- stop_action: true,
- metrics_path: '/root/hello-prometheus/environments/35/metrics',
- environment_path: '/root/hello-prometheus/environments/35',
- stop_path: '/root/hello-prometheus/environments/35/stop',
- terminal_path: '/root/hello-prometheus/environments/35/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/review',
- created_at: '2018-07-03T18:39:41.702Z',
- updated_at: '2018-07-03T18:44:54.010Z',
- last_deployment: {
- id: 128,
- },
+ id: 35,
+ name: 'review/noop-branch',
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
+ environment_type: 'review',
+ stop_action: true,
+ metrics_path: '/root/hello-prometheus/environments/35/metrics',
+ environment_path: '/root/hello-prometheus/environments/35',
+ stop_path: '/root/hello-prometheus/environments/35/stop',
+ terminal_path: '/root/hello-prometheus/environments/35/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/review',
+ created_at: '2018-07-03T18:39:41.702Z',
+ updated_at: '2018-07-03T18:44:54.010Z',
+ last_deployment: {
+ id: 128,
},
},
{
- name: 'no-deployment',
- size: 1,
- latest: {
- id: 36,
- name: 'no-deployment/noop-branch',
- state: 'available',
- created_at: '2018-07-04T18:39:41.702Z',
- updated_at: '2018-07-04T18:44:54.010Z',
- },
+ id: 36,
+ name: 'no-deployment/noop-branch',
+ state: 'available',
+ created_at: '2018-07-04T18:39:41.702Z',
+ updated_at: '2018-07-04T18:44:54.010Z',
},
];
diff --git a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js
new file mode 100644
index 00000000000..c41b29fa788
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js
@@ -0,0 +1,33 @@
+import jumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+const localVue = createLocalVue();
+
+describe('jumpToNextDiscussionButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(jumpToNextDiscussionButton, {
+ localVue,
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('emits onClick event on button click', done => {
+ const button = wrapper.find({ ref: 'button' });
+
+ button.trigger('click');
+
+ localVue.nextTick(() => {
+ expect(wrapper.emitted()).toEqual({
+ onClick: [[]],
+ });
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_resolve_button_spec.js b/spec/javascripts/notes/components/discussion_resolve_button_spec.js
new file mode 100644
index 00000000000..5024f40ec5d
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_resolve_button_spec.js
@@ -0,0 +1,74 @@
+import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+
+const buttonTitle = 'Resolve discussion';
+
+describe('resolveDiscussionButton', () => {
+ let wrapper;
+ let localVue;
+
+ const factory = options => {
+ localVue = createLocalVue();
+ wrapper = shallowMount(resolveDiscussionButton, {
+ localVue,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ factory({
+ propsData: {
+ isResolving: false,
+ buttonTitle,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should emit a onClick event on button click', () => {
+ const button = wrapper.find({ ref: 'button' });
+
+ button.trigger('click');
+
+ expect(wrapper.emitted()).toEqual({
+ onClick: [[]],
+ });
+ });
+
+ it('should contain the provided button title', () => {
+ const button = wrapper.find({ ref: 'button' });
+
+ expect(button.text()).toContain(buttonTitle);
+ });
+
+ it('should show a loading spinner while resolving', () => {
+ factory({
+ propsData: {
+ isResolving: true,
+ buttonTitle,
+ },
+ });
+
+ const button = wrapper.find({ ref: 'isResolvingIcon' });
+
+ expect(button.exists()).toEqual(true);
+ });
+
+ it('should only show a loading spinner while resolving', () => {
+ factory({
+ propsData: {
+ isResolving: false,
+ buttonTitle,
+ },
+ });
+
+ const button = wrapper.find({ ref: 'isResolvingIcon' });
+
+ localVue.nextTick(() => {
+ expect(button.exists()).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index f6c854e6def..b102b7aecf7 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -1,20 +1,19 @@
import Vue from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
-describe('issue_note_actions component', () => {
- let vm;
+describe('noteActions', () => {
+ let wrapper;
let store;
- let Component;
beforeEach(() => {
- Component = Vue.extend(noteActions);
store = createStore();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('user is logged in', () => {
@@ -36,45 +35,57 @@ describe('issue_note_actions component', () => {
store.dispatch('setUserData', userDataMock);
- vm = new Component({
+ const localVue = createLocalVue();
+ wrapper = shallowMount(noteActions, {
store,
propsData: props,
- }).$mount();
+ localVue,
+ sync: false,
+ });
});
it('should render access level badge', () => {
- expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel);
+ expect(
+ wrapper
+ .find('.note-role')
+ .text()
+ .trim(),
+ ).toEqual(props.accessLevel);
});
it('should render emoji link', () => {
- expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
+ expect(wrapper.find('.js-add-award').exists()).toBe(true);
});
describe('actions dropdown', () => {
it('should be possible to edit the comment', () => {
- expect(vm.$el.querySelector('.js-note-edit')).toBeDefined();
+ expect(wrapper.find('.js-note-edit').exists()).toBe(true);
});
it('should be possible to report abuse to GitLab', () => {
- expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined();
+ expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true);
});
it('should be possible to copy link to a note', () => {
- expect(vm.$el.querySelector('.js-btn-copy-note-link')).not.toBeNull();
+ expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(true);
});
it('should not show copy link action when `noteUrl` prop is empty', done => {
- vm.noteUrl = '';
+ wrapper.setProps({
+ ...props,
+ noteUrl: '',
+ });
+
Vue.nextTick()
.then(() => {
- expect(vm.$el.querySelector('.js-btn-copy-note-link')).toBeNull();
+ expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(false);
})
.then(done)
.catch(done.fail);
});
it('should be possible to delete comment', () => {
- expect(vm.$el.querySelector('.js-note-delete')).toBeDefined();
+ expect(wrapper.find('.js-note-delete').exists()).toBe(true);
});
});
});
@@ -96,18 +107,21 @@ describe('issue_note_actions component', () => {
reportAbusePath:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
};
- vm = new Component({
+ const localVue = createLocalVue();
+ wrapper = shallowMount(noteActions, {
store,
propsData: props,
- }).$mount();
+ localVue,
+ sync: false,
+ });
});
it('should not render emoji link', () => {
- expect(vm.$el.querySelector('.js-add-award')).toEqual(null);
+ expect(wrapper.find('.js-add-award').exists()).toBe(false);
});
it('should not render actions dropdown', () => {
- expect(vm.$el.querySelector('.more-actions')).toEqual(null);
+ expect(wrapper.find('.more-actions').exists()).toBe(false);
});
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 3aff2dd0641..c4b7eb17393 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import '~/behaviors/markdown/render_gfm';
@@ -8,9 +8,8 @@ import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
describe('noteable_discussion component', () => {
- const Component = Vue.extend(noteableDiscussion);
let store;
- let vm;
+ let wrapper;
preloadFixtures(discussionWithTwoUnresolvedNotes);
@@ -20,54 +19,62 @@ describe('noteable_discussion component', () => {
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
- vm = new Component({
+ const localVue = createLocalVue();
+ wrapper = shallowMount(noteableDiscussion, {
store,
propsData: { discussion: discussionMock },
- }).$mount();
+ localVue,
+ sync: false,
+ });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render user avatar', () => {
- expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
+ expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
});
it('should not render discussion header for non diff discussions', () => {
- expect(vm.$el.querySelector('.discussion-header')).toBeNull();
+ expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
- it('should render discussion header', () => {
+ it('should render discussion header', done => {
const discussion = { ...discussionMock };
discussion.diff_file = mockDiffFile;
discussion.diff_discussion = true;
- vm.$destroy();
- vm = new Component({
- store,
- propsData: { discussion },
- }).$mount();
+ wrapper.setProps({ discussion });
- expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.find('.discussion-header').exists()).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
describe('actions', () => {
it('should render reply button', () => {
- expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual(
- 'Reply...',
- );
+ expect(
+ wrapper
+ .find('.js-vue-discussion-reply')
+ .text()
+ .trim(),
+ ).toEqual('Reply...');
});
it('should toggle reply form', done => {
- vm.$el.querySelector('.js-vue-discussion-reply').click();
+ wrapper.find('.js-vue-discussion-reply').trigger('click');
- Vue.nextTick(() => {
- expect(vm.isReplying).toEqual(true);
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.isReplying).toEqual(true);
// There is a watcher for `isReplying` which will init autosave in the next tick
- Vue.nextTick(() => {
- expect(vm.$refs.noteForm).not.toBeNull();
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$refs.noteForm).not.toBeNull();
done();
});
});
@@ -75,8 +82,8 @@ describe('noteable_discussion component', () => {
it('does not render jump to discussion button', () => {
expect(
- vm.$el.querySelector('*[data-original-title="Jump to next unresolved discussion"]'),
- ).toBeNull();
+ wrapper.find('*[data-original-title="Jump to next unresolved discussion"]').exists(),
+ ).toBe(false);
});
});
@@ -87,12 +94,13 @@ describe('noteable_discussion component', () => {
discussion2.resolved = false;
discussion2.active = true;
discussion2.id = 'next'; // prepare this for being identified as next one (to be jumped to)
- vm.$store.dispatch('setInitialNotes', [discussionMock, discussion2]);
+ store.dispatch('setInitialNotes', [discussionMock, discussion2]);
window.mrTabs.currentAction = 'show';
- Vue.nextTick()
+ wrapper.vm
+ .$nextTick()
.then(() => {
- spyOn(vm, 'expandDiscussion').and.stub();
+ spyOn(wrapper.vm, 'expandDiscussion').and.stub();
const nextDiscussionId = discussion2.id;
@@ -100,9 +108,11 @@ describe('noteable_discussion component', () => {
<div class="discussion" data-discussion-id="${nextDiscussionId}"></div>
`);
- vm.jumpToNextDiscussion();
+ wrapper.vm.jumpToNextDiscussion();
- expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId });
+ expect(wrapper.vm.expandDiscussion).toHaveBeenCalledWith({
+ discussionId: nextDiscussionId,
+ });
})
.then(done)
.catch(done.fail);
@@ -117,7 +127,7 @@ describe('noteable_discussion component', () => {
notes: [{ body: 'hello world!' }],
};
- const note = vm.componentData(data);
+ const note = wrapper.vm.componentData(data);
expect(note).toEqual(data.notes[0]);
});
@@ -127,7 +137,7 @@ describe('noteable_discussion component', () => {
notes: [{ id: 12 }],
};
- const note = vm.componentData(data);
+ const note = wrapper.vm.componentData(data);
expect(note).toEqual(data);
});
@@ -138,46 +148,48 @@ describe('noteable_discussion component', () => {
const truncatedCommitId = commitId.substr(0, 8);
let commitElement;
- beforeEach(() => {
- vm.$destroy();
-
+ beforeEach(done => {
store.state.diffs = {
projectPath: 'something',
};
- vm = new Component({
- propsData: {
- discussion: {
- ...discussionMock,
- for_commit: true,
- commit_id: commitId,
- diff_discussion: true,
- diff_file: {
- ...mockDiffFile,
- },
+ wrapper.setProps({
+ discussion: {
+ ...discussionMock,
+ for_commit: true,
+ commit_id: commitId,
+ diff_discussion: true,
+ diff_file: {
+ ...mockDiffFile,
},
- renderDiffFile: true,
},
- store,
- }).$mount();
+ renderDiffFile: true,
+ });
- commitElement = vm.$el.querySelector('.commit-sha');
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ commitElement = wrapper.find('.commit-sha');
+ })
+ .then(done)
+ .catch(done.fail);
});
describe('for commit discussions', () => {
it('should display a monospace started a discussion on commit', () => {
- expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`);
- expect(commitElement).not.toBe(null);
- expect(commitElement).toHaveText(truncatedCommitId);
+ expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`);
+ expect(commitElement.exists()).toBe(true);
+ expect(commitElement.text()).toContain(truncatedCommitId);
});
});
describe('for diff discussion with a commit id', () => {
it('should display started discussion on commit header', done => {
- vm.discussion.for_commit = false;
+ wrapper.vm.discussion.for_commit = false;
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`);
- vm.$nextTick(() => {
- expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`);
expect(commitElement).not.toBe(null);
done();
@@ -185,11 +197,11 @@ describe('noteable_discussion component', () => {
});
it('should display outdated change on commit header', done => {
- vm.discussion.for_commit = false;
- vm.discussion.active = false;
+ wrapper.vm.discussion.for_commit = false;
+ wrapper.vm.discussion.active = false;
- vm.$nextTick(() => {
- expect(vm.$el).toContainText(
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain(
`started a discussion on an outdated change in commit ${truncatedCommitId}`,
);
@@ -202,27 +214,27 @@ describe('noteable_discussion component', () => {
describe('for diff discussions without a commit id', () => {
it('should show started a discussion on the diff text', done => {
- Object.assign(vm.discussion, {
+ Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
});
- vm.$nextTick(() => {
- expect(vm.$el).toContainText('started a discussion on the diff');
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain('started a discussion on the diff');
done();
});
});
it('should show discussion on older version text', done => {
- Object.assign(vm.discussion, {
+ Object.assign(wrapper.vm.discussion, {
for_commit: false,
commit_id: null,
active: false,
});
- vm.$nextTick(() => {
- expect(vm.$el).toContainText('started a discussion on an old version of the diff');
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain('started a discussion on an old version of the diff');
done();
});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 547379dabed..b2b0a50911d 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -21,6 +21,16 @@ Vue.config.productionTip = false;
let hasVueWarnings = false;
Vue.config.warnHandler = (msg, vm, trace) => {
+ // The following workaround is necessary, so we are able to use setProps from Vue test utils
+ // see https://github.com/vuejs/vue-test-utils/issues/631#issuecomment-421108344
+ const currentStack = new Error().stack;
+ const isInVueTestUtils = currentStack
+ .split('\n')
+ .some(line => line.startsWith(' at VueWrapper.setProps ('));
+ if (isInVueTestUtils) {
+ return;
+ }
+
hasVueWarnings = true;
fail(`${msg}${trace}`);
};
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
index de3e0c149de..e8b41e8eeff 100644
--- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -122,7 +122,7 @@ describe('User Popover Component', () => {
describe('status data', () => {
it('should show only message', () => {
const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.status = { message: 'Hello World' };
+ testProps.user.status = { message_html: 'Hello World' };
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
@@ -134,12 +134,12 @@ describe('User Popover Component', () => {
it('should show message and emoji', () => {
const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.status = { emoji: 'basketball_player', message: 'Hello World' };
+ testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' };
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
- status: { emoji: 'basketball_player', message: 'Hello World' },
+ status: { emoji: 'basketball_player', message_html: 'Hello World' },
});
expect(vm.$el.textContent).toContain('Hello World');
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index 7a457403b51..6217381c491 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -188,6 +188,22 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a')['class']).to eq 'custom'
end
+ it 'escapes RTLO and other characters' do
+ # rendered text looks like "http://example.com/evilexe.mp3"
+ evil_link = "#{link}evil\u202E3pm.exe"
+ doc = filter("#{evil_link}")
+
+ expect(doc.at_css('a')['href']).to eq "http://about.gitlab.com/evil%E2%80%AE3pm.exe"
+ end
+
+ it 'encodes international domains' do
+ link = "http://one😄two.com"
+ expected = "http://one%F0%9F%98%84two.com"
+ doc = filter(link)
+
+ expect(doc.at_css('a')['href']).to eq expected
+ end
+
described_class::IGNORE_PARENTS.each do |elem|
it "ignores valid links contained inside '#{elem}' element" do
exp = act = "<#{elem}>See #{link}</#{elem}>"
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index e6dae8d5382..2acbe05f082 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -62,6 +62,13 @@ describe Banzai::Filter::ExternalLinkFilter do
expect(doc.to_html).to eq(expected)
end
+
+ it 'adds rel and target to improperly formatted autolinks' do
+ doc = filter %q(<p><a href="mailto://jblogs@example.com">mailto://jblogs@example.com</a></p>)
+ expected = %q(<p><a href="mailto://jblogs@example.com" rel="nofollow noreferrer noopener" target="_blank">mailto://jblogs@example.com</a></p>)
+
+ expect(doc.to_html).to eq(expected)
+ end
end
context 'for links with a username' do
@@ -112,4 +119,62 @@ describe Banzai::Filter::ExternalLinkFilter do
it_behaves_like 'an external link with rel attribute'
end
+
+ context 'links with RTLO character' do
+ # In rendered text this looks like "http://example.com/evilexe.mp3"
+ let(:doc) { filter %Q(<a href="http://example.com/evil%E2%80%AE3pm.exe">http://example.com/evil\u202E3pm.exe</a>) }
+
+ it_behaves_like 'an external link with rel attribute'
+
+ it 'escapes RTLO in link text' do
+ expected = %q(http://example.com/evil%E2%80%AE3pm.exe</a>)
+
+ expect(doc.to_html).to include(expected)
+ end
+
+ it 'does not mangle the link text' do
+ doc = filter %Q(<a href="http://example.com">One<span>and</span>\u202Eexe.mp3</a>)
+
+ expect(doc.to_html).to include('One<span>and</span>%E2%80%AEexe.mp3</a>')
+ end
+ end
+
+ context 'for generated autolinks' do
+ context 'with an IDN character' do
+ let(:doc) { filter(%q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>)) }
+ let(:doc_email) { filter(%q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>), emailable_links: true) }
+
+ it_behaves_like 'an external link with rel attribute'
+
+ it 'does not change the link text' do
+ expect(doc.to_html).to include('http://exa😄mple.com</a>')
+ end
+
+ it 'uses punycode for emails' do
+ expect(doc_email.to_html).to include('http://xn--example-6p25f.com/</a>')
+ end
+ end
+ end
+
+ context 'for links that look malicious' do
+ context 'with an IDN character' do
+ let(:doc) { filter %q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>) }
+
+ it 'adds a toolip with punycode' do
+ expect(doc.to_html).to include('http://exa😄mple.com</a>')
+ expect(doc.to_html).to include('class="has-tooltip"')
+ expect(doc.to_html).to include('title="http://xn--example-6p25f.com/"')
+ end
+ end
+
+ context 'with RTLO character' do
+ let(:doc) { filter %q(<a href="http://example.com/evil%E2%80%AE3pm.exe">Evil Test</a>) }
+
+ it 'adds a toolip with punycode' do
+ expect(doc.to_html).to include('Evil Test</a>')
+ expect(doc.to_html).to include('class="has-tooltip"')
+ expect(doc.to_html).to include('title="http://example.com/evil%E2%80%AE3pm.exe"')
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index cf49249756a..4c4e821deab 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -30,21 +30,21 @@ describe Banzai::Filter::MarkdownFilter do
end
it 'adds language to lang attribute when specified' do
- result = filter("```html\nsome code\n```")
+ result = filter("```html\nsome code\n```", no_sourcepos: true)
- expect(result).to start_with("<pre><code lang=\"html\">")
+ expect(result).to start_with('<pre><code lang="html">')
end
it 'does not add language to lang attribute when not specified' do
- result = filter("```\nsome code\n```")
+ result = filter("```\nsome code\n```", no_sourcepos: true)
- expect(result).to start_with("<pre><code>")
+ expect(result).to start_with('<pre><code>')
end
it 'works with utf8 chars in language' do
- result = filter("```æ—¥\nsome code\n```")
+ result = filter("```æ—¥\nsome code\n```", no_sourcepos: true)
- expect(result).to start_with("<pre><code lang=\"æ—¥\">")
+ expect(result).to start_with('<pre><code lang="æ—¥">')
end
end
@@ -67,6 +67,38 @@ describe Banzai::Filter::MarkdownFilter do
end
end
+ describe 'source line position' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
+
+ it 'defaults to add data-sourcepos' do
+ result = filter('test')
+
+ expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>'
+ end
+
+ it 'disables data-sourcepos' do
+ result = filter('test', no_sourcepos: true)
+
+ expect(result).to eq '<p>test</p>'
+ end
+ end
+
+ context 'using Redcarpet' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet)
+ end
+
+ it 'does not support data-sourcepos' do
+ result = filter('test')
+
+ expect(result).to eq '<p>test</p>'
+ end
+ end
+ end
+
describe 'footnotes in tables' do
it 'processes footnotes in table cells' do
text = <<-MD.strip_heredoc
@@ -77,7 +109,7 @@ describe Banzai::Filter::MarkdownFilter do
[^1]: a footnote
MD
- result = filter(text)
+ result = filter(text, no_sourcepos: true)
expect(result).to include('<td>foot <sup')
expect(result).to include('<section class="footnotes">')
diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb
index c68d49f9366..69f9c1ae829 100644
--- a/spec/lib/banzai/filter/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb
@@ -26,6 +26,12 @@ describe Banzai::Filter::ProjectReferenceFilter do
expect(reference_filter(act).to_html).to eq(CGI.escapeHTML(exp))
end
+ it 'fails fast for long invalid string' do
+ expect do
+ Timeout.timeout(5.seconds) { reference_filter("A" * 50000).to_html }
+ end.not_to raise_error
+ end
+
it 'allows references with text after the > character' do
doc = reference_filter("Hey #{reference}foo")
expect(doc.css('a').first.attr('href')).to eq urls.project_url(subject)
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 836af18e0b6..f2a5d7b2c9f 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -301,6 +301,13 @@ describe Banzai::Filter::SanitizationFilter do
expect(act.to_html).to eq exp
end
+ it 'allows the `data-sourcepos` attribute globally' do
+ exp = %q{<p data-sourcepos="1:1-1:10">foo/bar.md</p>}
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+
describe 'footnotes' do
it 'allows correct footnote id property on links' do
exp = %q{<a href="#fn1" id="fnref1">foo/bar.md</a>}
diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
index 8cce1b96698..77cb1954ea3 100644
--- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
@@ -13,6 +13,10 @@ describe Banzai::Pipeline::DescriptionPipeline do
output
end
+ before do
+ stub_commonmark_sourcepos_disabled
+ end
+
it 'uses a limited whitelist' do
doc = parse('# Description')
diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
index 6a11ca2f9d5..b99161109eb 100644
--- a/spec/lib/banzai/pipeline/email_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
@@ -10,5 +10,19 @@ describe Banzai::Pipeline::EmailPipeline do
expect(described_class.filters).not_to be_empty
expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter)
end
+
+ it 'shows punycode for autolinks' do
+ examples = %W[
+ http://one😄two.com
+ http://\u0261itlab.com
+ ]
+
+ examples.each do |markdown|
+ result = described_class.call(markdown, project: nil)[:output]
+ link = result.css('a').first
+
+ expect(link.content).to include('http://xn--')
+ end
+ end
end
end
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index 3634655c6a5..3d3aa64d630 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -54,7 +54,47 @@ describe Banzai::Pipeline::FullPipeline do
end
it 'properly adds the necessary ids and classes' do
+ stub_commonmark_sourcepos_disabled
+
expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote
end
end
+
+ describe 'links are detected as malicious' do
+ it 'has tooltips for malicious links' do
+ examples = %W[
+ http://example.com/evil\u202E3pm.exe
+ [evilexe.mp3](http://example.com/evil\u202E3pm.exe)
+ rdar://localhost.com/\u202E3pm.exe
+ http://one😄two.com
+ [Evil-Test](http://one😄two.com)
+ http://\u0261itlab.com
+ [Evil-GitLab-link](http://\u0261itlab.com)
+ ![Evil-GitLab-link](http://\u0261itlab.com.png)
+ ]
+
+ examples.each do |markdown|
+ result = described_class.call(markdown, project: nil)[:output]
+ link = result.css('a').first
+
+ expect(link[:class]).to include('has-tooltip')
+ end
+ end
+
+ it 'has no tooltips for safe links' do
+ examples = %w[
+ http://example.com
+ [Safe-Test](http://example.com)
+ https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg
+ [Wikipedia-link](https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg)
+ ]
+
+ examples.each do |markdown|
+ result = described_class.call(markdown, project: nil)[:output]
+ link = result.css('a').first
+
+ expect(link[:class]).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 3459939267a..0302e4090cf 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -163,14 +163,14 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
end
- it 'raises exception' do
+ it 'wastes pipeline iid' do
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
- end
- it 'wastes pipeline iid' do
- expect { step.perform! }.to raise_error
+ last_iid = InternalId.ci_pipelines
+ .where(project_id: project.id)
+ .last.last_value
- expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
+ expect(last_iid).to be > 0
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index a700cfd4546..fae8add6453 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -5,8 +5,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:attributes) do
- { name: 'rspec',
- ref: 'master' }
+ { name: 'rspec', ref: 'master' }
end
subject do
@@ -21,10 +20,45 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
+ describe '#bridge?' do
+ context 'when job is a bridge' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
+ end
+
+ it { is_expected.to be_bridge }
+ end
+
+ context 'when trigger definition is empty' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { trigger: '' } }
+ end
+
+ it { is_expected.not_to be_bridge }
+ end
+
+ context 'when job is not a bridge' do
+ it { is_expected.not_to be_bridge }
+ end
+ end
+
describe '#to_resource' do
- it 'returns a valid build resource' do
- expect(subject.to_resource).to be_a(::Ci::Build)
- expect(subject.to_resource).to be_valid
+ context 'when job is not a bridge' do
+ it 'returns a valid build resource' do
+ expect(subject.to_resource).to be_a(::Ci::Build)
+ expect(subject.to_resource).to be_valid
+ end
+ end
+
+ context 'when job is a bridge' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
+ end
+
+ it 'returns a valid bridge resource' do
+ expect(subject.to_resource).to be_a(::Ci::Bridge)
+ expect(subject.to_resource).to be_valid
+ end
end
it 'memoizes a resource object' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 82f741845db..493ca3cd7b5 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -62,8 +62,18 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
- expect(subject.seeds.map(&:attributes))
- .to all(include(trigger_request: pipeline.trigger_requests.first))
+ end
+
+ context 'when a legacy trigger exists' do
+ before do
+ create(:ci_trigger_request, pipeline: pipeline)
+ end
+
+ it 'returns build seeds including legacy trigger' do
+ expect(pipeline.legacy_trigger).not_to be_nil
+ expect(subject.seeds.map(&:attributes))
+ .to all(include(trigger_request: pipeline.legacy_trigger))
+ end
end
context 'when a ref is protected' do
diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb
index 40871f86568..0d02c371a92 100644
--- a/spec/lib/gitlab/ci/status/external/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/external/common_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Ci::Status::External::Common do
end
subject do
- Gitlab::Ci::Status::Core
+ Gitlab::Ci::Status::Success
.new(external_status, user)
.extend(described_class)
end
@@ -20,6 +20,22 @@ describe Gitlab::Ci::Status::External::Common do
it 'returns description' do
expect(subject.label).to eq external_description
end
+
+ context 'when description is nil' do
+ let(:external_description) { nil }
+
+ it 'uses core status label' do
+ expect(subject.label).to eq('passed')
+ end
+ end
+
+ context 'when description is empty string' do
+ let(:external_description) { '' }
+
+ it 'uses core status label' do
+ expect(subject.label).to eq('passed')
+ end
+ end
end
describe '#has_action?' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 20c35573cfb..91139d421f5 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -21,15 +21,12 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"]
},
allow_failure: false,
when: "on_success",
- environment: nil,
yaml_variables: []
})
end
@@ -154,12 +151,9 @@ module Gitlab
builds:
[{ stage_idx: 1,
stage: "test",
- tag_list: [],
name: "rspec",
allow_failure: false,
when: "on_success",
- environment: nil,
- coverage_regex: nil,
yaml_variables: [],
options: { script: ["rspec"] },
only: { refs: ["branches"] },
@@ -169,12 +163,9 @@ module Gitlab
builds:
[{ stage_idx: 2,
stage: "deploy",
- tag_list: [],
name: "prod",
allow_failure: false,
when: "on_success",
- environment: nil,
- coverage_regex: nil,
yaml_variables: [],
options: { script: ["cap prod"] },
only: { refs: ["tags"] },
@@ -344,8 +335,6 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"],
@@ -356,7 +345,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- environment: nil,
yaml_variables: []
})
end
@@ -378,8 +366,6 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"],
@@ -390,7 +376,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- environment: nil,
yaml_variables: []
})
end
@@ -410,8 +395,6 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"],
@@ -420,7 +403,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- environment: nil,
yaml_variables: []
})
end
@@ -438,8 +420,6 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"],
@@ -448,7 +428,6 @@ module Gitlab
},
allow_failure: false,
when: "on_success",
- environment: nil,
yaml_variables: []
})
end
@@ -763,8 +742,6 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "rspec",
- coverage_regex: nil,
- tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"],
@@ -779,7 +756,6 @@ module Gitlab
},
when: "on_success",
allow_failure: false,
- environment: nil,
yaml_variables: []
})
end
@@ -976,14 +952,11 @@ module Gitlab
stage: "test",
stage_idx: 1,
name: "normal_job",
- coverage_regex: nil,
- tag_list: [],
options: {
script: ["test"]
},
when: "on_success",
allow_failure: false,
- environment: nil,
yaml_variables: []
})
end
@@ -1023,28 +996,22 @@ module Gitlab
stage: "build",
stage_idx: 0,
name: "job1",
- coverage_regex: nil,
- tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
- environment: nil,
yaml_variables: []
})
expect(subject.second).to eq({
stage: "build",
stage_idx: 0,
name: "job2",
- coverage_regex: nil,
- tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
- environment: nil,
yaml_variables: []
})
end
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 98f1696badb..9ef987a0826 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -37,7 +37,7 @@ describe Gitlab::DataBuilder::Pipeline do
context 'pipeline without variables' do
it 'has empty variables hash' do
expect(attributes[:variables]).to be_a(Array)
- expect(attributes[:variables]).to be_empty()
+ expect(attributes[:variables]).to be_empty
end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index befdc18d1aa..0c4decc6518 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::DataBuilder::Push do
let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let(:user) { build(:user, public_email: 'public-email@example.com') }
describe '.build_sample' do
let(:data) { described_class.build_sample(project, user) }
@@ -36,7 +36,7 @@ describe Gitlab::DataBuilder::Push do
it { expect(data[:user_id]).to eq(user.id) }
it { expect(data[:user_name]).to eq(user.name) }
it { expect(data[:user_username]).to eq(user.username) }
- it { expect(data[:user_email]).to eq(user.email) }
+ it { expect(data[:user_email]).to eq(user.public_email) }
it { expect(data[:user_avatar]).to eq(user.avatar_url) }
it { expect(data[:project_id]).to eq(project.id) }
it { expect(data[:project]).to be_a(Hash) }
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index b1f48c15c21..e5420ea6bea 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -118,6 +118,43 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
end
end
+ shared_examples "checks permissions on noteable" do
+ context "when user has access" do
+ before do
+ project.add_reporter(user)
+ end
+
+ it "creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ end
+ end
+
+ context "when user does not have access" do
+ it "raises UserNotAuthorizedError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
+ end
+ end
+ end
+
+ context "when discussion is locked" do
+ before do
+ noteable.update_attribute(:discussion_locked, true)
+ end
+
+ it_behaves_like "checks permissions on noteable"
+ end
+
+ context "when issue is confidential" do
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note, noteable: issue, project: project) }
+
+ before do
+ issue.update_attribute(:confidential, true)
+ end
+
+ it_behaves_like "checks permissions on noteable"
+ end
+
context "when everything is fine" do
before do
setup_attachment
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 3e34dd592f2..634c370d211 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -776,10 +776,13 @@ describe Gitlab::GitAccess do
it "has the correct permissions for #{role}s" do
if role == :admin
user.update_attribute(:admin, true)
+ project.add_guest(user)
else
project.add_role(user, role)
end
+ protected_branch.save
+
aggregate_failures do
matrix.each do |action, allowed|
check = -> { push_changes(changes[action]) }
@@ -861,25 +864,19 @@ describe Gitlab::GitAccess do
[%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
context do
- before do
- create(:protected_branch, name: protected_branch_name, project: project)
- end
+ let(:protected_branch) { create(:protected_branch, :maintainers_can_push, name: protected_branch_name, project: project) }
run_permission_checks(permissions_matrix)
end
context "when developers are allowed to push into the #{protected_branch_type} protected branch" do
- before do
- create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project)
- end
+ let(:protected_branch) { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) }
run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
end
context "developers are allowed to merge into the #{protected_branch_type} protected branch" do
- before do
- create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project)
- end
+ let(:protected_branch) { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) }
context "when a merge request exists for the given source/target branch" do
context "when the merge request is in progress" do
@@ -906,17 +903,13 @@ describe Gitlab::GitAccess do
end
context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do
- before do
- create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project)
- end
+ let(:protected_branch) { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) }
run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
end
context "when no one is allowed to push to the #{protected_branch_name} protected branch" do
- before do
- create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project)
- end
+ let(:protected_branch) { build(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) }
run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
maintainer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index e41a75c37a7..cf12baf1a93 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -119,6 +119,15 @@ describe Gitlab::GitalyClient do
end
end
+ describe '.connection_data' do
+ it 'returns connection data' do
+ address = 'tcp://localhost:9876'
+ stub_repos_storages address
+
+ expect(described_class.connection_data('default')).to eq({ 'address' => address, 'token' => 'secret' })
+ end
+ end
+
describe 'allow_n_plus_1_calls' do
context 'when RequestStore is enabled', :request_store do
it 'returns the result of the allow_n_plus_1_calls block' do
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 861710f7e9b..91229d9c7d4 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -58,17 +58,5 @@ describe Gitlab::GithubImport::BulkImporting do
importer.bulk_insert(model, rows, batch_size: 5)
end
-
- it 'calls pre_hook for each slice if given' do
- rows = [{ title: 'Foo' }] * 10
- model = double(:model, table_name: 'kittens')
- pre_hook = double('pre_hook', call: nil)
- allow(Gitlab::Database).to receive(:bulk_insert)
-
- expect(pre_hook).to receive(:call).with(rows[0..4])
- expect(pre_hook).to receive(:call).with(rows[5..9])
-
- importer.bulk_insert(model, rows, batch_size: 5, pre_hook: pre_hook)
- end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index 65a2e1cb5cb..7901ae005d9 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -78,11 +78,6 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
.to receive(:id_for)
.with(issue)
.and_return(milestone.id)
-
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .with(issue)
- .and_return([user.id, true])
end
context 'when the issue author could be found' do
@@ -177,23 +172,6 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
expect(importer.create_issue).to be_a_kind_of(Numeric)
end
-
- it 'triggers internal_id functionality to track greatest iids' do
- allow(importer.user_finder)
- .to receive(:author_id_for)
- .with(issue)
- .and_return([user.id, true])
-
- issue = build_stubbed(:issue, project: project)
- allow(importer)
- .to receive(:insert_and_return_id)
- .and_return(issue.id)
- allow(project.issues).to receive(:find).with(issue.id).and_return(issue)
-
- expect(issue).to receive(:ensure_project_iid!)
-
- importer.create_issue
- end
end
describe '#create_assignees' do
diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
index 4857f2afbe2..8fd328d9c1e 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
@@ -2,20 +2,26 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer::LfsObjectImporter do
let(:project) { create(:project) }
- let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" }
-
- let(:github_lfs_object) do
- Gitlab::GithubImport::Representation::LfsObject.new(
- oid: 'oid', download_link: download_link
- )
+ let(:lfs_attributes) do
+ {
+ oid: 'oid',
+ size: 1,
+ link: 'http://www.gitlab.com/lfs_objects/oid'
+ }
end
+ let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) }
+ let(:github_lfs_object) { Gitlab::GithubImport::Representation::LfsObject.new(lfs_attributes) }
+
let(:importer) { described_class.new(github_lfs_object, project, nil) }
describe '#execute' do
it 'calls the LfsDownloadService with the lfs object attributes' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService)
- .to receive(:execute).with('oid', download_link)
+ allow(importer).to receive(:lfs_download_object).and_return(lfs_download_object)
+
+ service = double
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).with(project, lfs_download_object).and_return(service)
+ expect(service).to receive(:execute)
importer.execute
end
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index 5f5c6b803c0..50442552eee 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -5,7 +5,15 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
let(:client) { double(:client) }
let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" }
- let(:github_lfs_object) { ['oid', download_link] }
+ let(:lfs_attributes) do
+ {
+ oid: 'oid',
+ size: 1,
+ link: 'http://www.gitlab.com/lfs_objects/oid'
+ }
+ end
+
+ let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) }
describe '#parallel?' do
it 'returns true when running in parallel mode' do
@@ -48,7 +56,7 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
allow(importer)
.to receive(:each_object_to_import)
- .and_yield(['oid', download_link])
+ .and_yield(lfs_download_object)
expect(Gitlab::GithubImport::Importer::LfsObjectImporter)
.to receive(:new)
@@ -71,7 +79,7 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
allow(importer)
.to receive(:each_object_to_import)
- .and_yield(github_lfs_object)
+ .and_yield(lfs_download_object)
expect(Gitlab::GithubImport::ImportLfsObjectWorker)
.to receive(:perform_async)
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index db0be760c7b..b1cac3b6e46 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -29,25 +29,13 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
expect(importer)
.to receive(:bulk_insert)
- .with(Milestone, [milestone_hash], any_args)
+ .with(Milestone, [milestone_hash])
expect(importer)
.to receive(:build_milestones_cache)
importer.execute
end
-
- it 'tracks internal ids' do
- milestone_hash = { iid: 1, title: '1.0', project_id: project.id }
- allow(importer)
- .to receive(:build_milestones)
- .and_return([milestone_hash])
-
- expect(InternalId).to receive(:track_greatest)
- .with(nil, { project: project }, :milestones, 1, any_args)
-
- importer.execute
- end
end
describe '#build_milestones' do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 25684ea9e2c..0f21b8843b6 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -111,16 +111,6 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
expect(mr).to be_instance_of(MergeRequest)
expect(exists).to eq(false)
end
-
- it 'triggers internal_id functionality to track greatest iids' do
- mr = build_stubbed(:merge_request, source_project: project, target_project: project)
- allow(importer).to receive(:insert_and_return_id).and_return(mr.id)
- allow(project.merge_requests).to receive(:find).with(mr.id).and_return(mr)
-
- expect(mr).to receive(:ensure_target_project_iid!)
-
- importer.create_merge_request
- end
end
context 'when the author could not be found' do
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 01d43ed00a2..3942f168ceb 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::HashedStorage::Migrator do
describe '#bulk_schedule' do
it 'schedules job to StorageMigratorWorker' do
Sidekiq::Testing.fake! do
- expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1)
+ expect { subject.bulk_schedule(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1)
end
end
end
@@ -15,13 +15,13 @@ describe Gitlab::HashedStorage::Migrator do
it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
Sidekiq::Testing.fake! do
- expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2)
+ expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2)
end
end
it 'rescues and log exceptions' do
allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
- expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error
+ expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.not_to raise_error
end
it 'delegates each project in specified range to #migrate' do
@@ -29,12 +29,12 @@ describe Gitlab::HashedStorage::Migrator do
expect(subject).to receive(:migrate).with(project)
end
- subject.bulk_migrate(ids.min, ids.max)
+ subject.bulk_migrate(start: ids.min, finish: ids.max)
end
it 'has migrated projects set as writable' do
perform_enqueued_jobs do
- subject.bulk_migrate(ids.min, ids.max)
+ subject.bulk_migrate(start: ids.min, finish: ids.max)
end
projects.each do |project|
@@ -46,7 +46,7 @@ describe Gitlab::HashedStorage::Migrator do
describe '#migrate' do
let(:project) { create(:project, :legacy_storage, :empty_repo) }
- it 'enqueues job to ProjectMigrateHashedStorageWorker' do
+ it 'enqueues project migration job' do
Sidekiq::Testing.fake! do
expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
end
@@ -58,7 +58,7 @@ describe Gitlab::HashedStorage::Migrator do
expect { subject.migrate(project) }.not_to raise_error
end
- it 'migrate project' do
+ it 'migrates project storage' do
perform_enqueued_jobs do
subject.migrate(project)
end
@@ -73,5 +73,19 @@ describe Gitlab::HashedStorage::Migrator do
expect(project.reload.repository_read_only?).to be_falsey
end
+
+ context 'when project is already on hashed storage' do
+ let(:project) { create(:project, :empty_repo) }
+
+ it 'doesnt enqueue any migration job' do
+ Sidekiq::Testing.fake! do
+ expect { subject.migrate(project) }.not_to change(ProjectMigrateHashedStorageWorker.jobs, :size)
+ end
+ end
+
+ it 'returns false' do
+ expect(subject.migrate(project)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5afa9669b1a..6897ac8a3a8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -114,6 +114,7 @@ ci_pipelines:
- stages
- statuses
- builds
+- processables
- trigger_requests
- variables
- auto_canceled_by
@@ -137,6 +138,7 @@ stages:
- pipeline
- statuses
- builds
+- bridges
statuses:
- project
- pipeline
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 242c16c4bdc..6084dc96410 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
]
RSpec::Mocks.with_temporary_scope do
- @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
+ @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
@@ -40,7 +40,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project = Project.find_by_path('project')
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
- expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.builds_access_level).to eq(ProjectFeature::ENABLED)
expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
@@ -273,6 +273,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'has group milestone' do
expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
end
+
+ it 'has the correct visibility level' do
+ # INTERNAL in the `project.json`, group's is PRIVATE
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
end
context 'Light JSON' do
@@ -347,7 +352,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
:issues_disabled,
name: 'project',
path: 'project',
- group: create(:group))
+ group: create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE))
end
before do
@@ -434,4 +439,58 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
end
+
+ describe '#restored_project' do
+ let(:project) { create(:project) }
+ let(:shared) { project.import_export_shared }
+ let(:tree_hash) { { 'visibility_level' => visibility } }
+ let(:restorer) { described_class.new(user: nil, shared: shared, project: project) }
+
+ before do
+ restorer.instance_variable_set(:@tree_hash, tree_hash)
+ end
+
+ context 'no group visibility' do
+ let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
+
+ it 'uses the project visibility' do
+ expect(restorer.restored_project.visibility_level).to eq(visibility)
+ end
+ end
+
+ context 'with group visibility' do
+ before do
+ group = create(:group, visibility_level: group_visibility)
+
+ project.update(group: group)
+ end
+
+ context 'private group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+
+ it 'uses the group visibility' do
+ expect(restorer.restored_project.visibility_level).to eq(group_visibility)
+ end
+ end
+
+ context 'public group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
+
+ it 'uses the project visibility' do
+ expect(restorer.restored_project.visibility_level).to eq(visibility)
+ end
+ end
+
+ context 'internal group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+
+ it 'uses the group visibility' do
+ expect(restorer.restored_project.visibility_level).to eq(group_visibility)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
new file mode 100644
index 00000000000..f2d750c6595
--- /dev/null
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe Gitlab::ImportExport::Shared do
+ let(:project) { build(:project) }
+ subject { project.import_export_shared }
+
+ describe '#error' do
+ let(:error) { StandardError.new('Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file') }
+
+ it 'filters any full paths' do
+ subject.error(error)
+
+ expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]'])
+ end
+
+ it 'calls the error logger with the full message' do
+ expect(subject).to receive(:log_error).with(hash_including(message: error.message))
+
+ subject.error(error)
+ end
+
+ it 'calls the debug logger with a backtrace' do
+ error.set_backtrace('backtrace')
+
+ expect(subject).to receive(:log_debug).with(hash_including(backtrace: 'backtrace'))
+
+ subject.error(error)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index 49d857d9483..76f8253ec9b 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::VersionChecker do
- let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
+ let!(:shared) { Gitlab::ImportExport::Shared.new(nil) }
describe 'bundle a project Git repo' do
let(:version) { Gitlab::ImportExport.version }
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 8fc85301304..02364e92149 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -24,6 +24,32 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ shared_examples 'redirection not allowed' do |method_name|
+ before do
+ redirect_url = 'https://not-under-our-control.example.com/api/v1/pods'
+
+ stub_request(:get, %r{\A#{api_url}/})
+ .to_return(status: 302, headers: { location: redirect_url })
+
+ stub_request(:get, redirect_url)
+ .to_return(status: 200, body: '{}')
+ end
+
+ it 'does not follow redirects' do
+ method_call = -> do
+ case method_name
+ when /\A(get_|delete_)/
+ client.public_send(method_name)
+ when /\A(create_|update_)/
+ client.public_send(method_name, {})
+ else
+ raise "Unknown method name #{method_name}"
+ end
+ end
+ expect { method_call.call }.to raise_error(Kubeclient::HttpError)
+ end
+ end
+
describe '#core_client' do
subject { client.core_client }
@@ -103,6 +129,8 @@ describe Gitlab::Kubernetes::KubeClient do
:update_service_account
].each do |method|
describe "##{method}" do
+ include_examples 'redirection not allowed', method
+
it 'delegates to the core client' do
expect(client).to delegate_method(method).to(:core_client)
end
@@ -123,6 +151,8 @@ describe Gitlab::Kubernetes::KubeClient do
:update_cluster_role_binding
].each do |method|
describe "##{method}" do
+ include_examples 'redirection not allowed', method
+
it 'delegates to the rbac client' do
expect(client).to delegate_method(method).to(:rbac_client)
end
@@ -139,6 +169,8 @@ describe Gitlab::Kubernetes::KubeClient do
let(:extensions_client) { client.extensions_client }
describe '#get_deployments' do
+ include_examples 'redirection not allowed', 'get_deployments'
+
it 'delegates to the extensions client' do
expect(client).to delegate_method(:get_deployments).to(:extensions_client)
end
diff --git a/spec/lib/gitlab/release_blog_post_spec.rb b/spec/lib/gitlab/release_blog_post_spec.rb
deleted file mode 100644
index 2c987df3767..00000000000
--- a/spec/lib/gitlab/release_blog_post_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::ReleaseBlogPost do
- describe '.blog_post_url' do
- let(:releases_xml) do
- <<~EOS
- <?xml version='1.0' encoding='utf-8' ?>
- <feed xmlns='http://www.w3.org/2005/Atom'>
- <entry>
- <release>11.2</release>
- <id>https://about.gitlab.com/2018/08/22/gitlab-11-2-released/</id>
- </entry>
- <entry>
- <release>11.1</release>
- <id>https://about.gitlab.com/2018/07/22/gitlab-11-1-released/</id>
- </entry>
- <entry>
- <release>11.0</release>
- <id>https://about.gitlab.com/2018/06/22/gitlab-11-0-released/</id>
- </entry>
- <entry>
- <release>10.8</release>
- <id>https://about.gitlab.com/2018/05/22/gitlab-10-8-released/</id>
- </entry>
- </feed>
- EOS
- end
-
- subject { described_class.send(:new).blog_post_url }
-
- before do
- stub_request(:get, 'https://about.gitlab.com/releases.xml')
- .to_return(status: 200, headers: { 'content-type' => ['text/xml'] }, body: releases_xml)
- end
-
- context 'matches GitLab version to blog post url' do
- it 'returns the correct url for major pre release' do
- stub_const('Gitlab::VERSION', '11.0.0-pre')
-
- expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/')
- end
-
- it 'returns the correct url for major release candidate' do
- stub_const('Gitlab::VERSION', '11.0.0-rc3')
-
- expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/')
- end
-
- it 'returns the correct url for major release' do
- stub_const('Gitlab::VERSION', '11.0.0')
-
- expect(subject).to eql('https://about.gitlab.com/2018/06/22/gitlab-11-0-released/')
- end
-
- it 'returns the correct url for minor pre release' do
- stub_const('Gitlab::VERSION', '11.2.0-pre')
-
- expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/')
- end
-
- it 'returns the correct url for minor release candidate' do
- stub_const('Gitlab::VERSION', '11.2.0-rc3')
-
- expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/')
- end
-
- it 'returns the correct url for minor release' do
- stub_const('Gitlab::VERSION', '11.2.0')
-
- expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/')
- end
-
- it 'returns the correct url for patch pre release' do
- stub_const('Gitlab::VERSION', '11.2.1-pre')
- expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/')
- end
-
- it 'returns the correct url for patch release candidate' do
- stub_const('Gitlab::VERSION', '11.2.1-rc3')
-
- expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/')
- end
-
- it 'returns the correct url for patch release' do
- stub_const('Gitlab::VERSION', '11.2.1')
-
- expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/')
- end
-
- it 'returns nil when no blog post is matched' do
- stub_const('Gitlab::VERSION', '9.0.0')
-
- expect(subject).to be(nil)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index a9d15f1d522..7bc4599e20f 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
"correlation_id" => 'cid'
}
end
- let(:logger) { double() }
+ let(:logger) { double }
let(:start_payload) do
job.merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
new file mode 100644
index 00000000000..c9d1a06b3e6
--- /dev/null
+++ b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Tracing::Rails::ActionViewSubscriber do
+ using RSpec::Parameterized::TableSyntax
+
+ shared_examples 'an actionview notification' do
+ it 'should notify the tracer when the hash contains null values' do
+ expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
+
+ subject.public_send(notify_method, start, finish, payload)
+ end
+
+ it 'should notify the tracer when the payload is missing values' do
+ expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
+
+ subject.public_send(notify_method, start, finish, payload.compact)
+ end
+
+ it 'should not throw exceptions when with the default tracer' do
+ expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error
+ end
+ end
+
+ describe '.instrument' do
+ it 'is unsubscribeable' do
+ unsubscribe = described_class.instrument
+
+ expect(unsubscribe).not_to be_nil
+ expect { unsubscribe.call }.not_to raise_error
+ end
+ end
+
+ describe '#notify_render_template' do
+ subject { described_class.new }
+ let(:start) { Time.now }
+ let(:finish) { Time.now }
+ let(:notification_name) { 'render_template' }
+ let(:notify_method) { :notify_render_template }
+
+ where(:identifier, :layout, :exception) do
+ nil | nil | nil
+ "" | nil | nil
+ "show.haml" | nil | nil
+ nil | "" | nil
+ nil | "layout.haml" | nil
+ nil | nil | StandardError.new
+ end
+
+ with_them do
+ let(:payload) do
+ {
+ exception: exception,
+ identifier: identifier,
+ layout: layout
+ }
+ end
+
+ let(:expected_tags) do
+ {
+ 'component' => 'ActionView',
+ 'template.id' => identifier,
+ 'template.layout' => layout
+ }
+ end
+
+ it_behaves_like 'an actionview notification'
+ end
+ end
+
+ describe '#notify_render_collection' do
+ subject { described_class.new }
+ let(:start) { Time.now }
+ let(:finish) { Time.now }
+ let(:notification_name) { 'render_collection' }
+ let(:notify_method) { :notify_render_collection }
+
+ where(
+ :identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do
+ nil | nil | 0 | nil | 0 | nil
+ "" | nil | 0 | nil | 0 | nil
+ "show.haml" | nil | 0 | nil | 0 | nil
+ nil | 0 | 0 | nil | 0 | nil
+ nil | 1 | 1 | nil | 0 | nil
+ nil | nil | 0 | 0 | 0 | nil
+ nil | nil | 0 | 1 | 1 | nil
+ nil | nil | 0 | nil | 0 | StandardError.new
+ end
+
+ with_them do
+ let(:payload) do
+ {
+ exception: exception,
+ identifier: identifier,
+ count: count,
+ cache_hits: cache_hits
+ }
+ end
+
+ let(:expected_tags) do
+ {
+ 'component' => 'ActionView',
+ 'template.id' => identifier,
+ 'template.count' => expected_count,
+ 'template.cache.hits' => expected_cache_hits
+ }
+ end
+
+ it_behaves_like 'an actionview notification'
+ end
+ end
+
+ describe '#notify_render_partial' do
+ subject { described_class.new }
+ let(:start) { Time.now }
+ let(:finish) { Time.now }
+ let(:notification_name) { 'render_partial' }
+ let(:notify_method) { :notify_render_partial }
+
+ where(:identifier, :exception) do
+ nil | nil
+ "" | nil
+ "show.haml" | nil
+ nil | StandardError.new
+ end
+
+ with_them do
+ let(:payload) do
+ {
+ exception: exception,
+ identifier: identifier
+ }
+ end
+
+ let(:expected_tags) do
+ {
+ 'component' => 'ActionView',
+ 'template.id' => identifier
+ }
+ end
+
+ it_behaves_like 'an actionview notification'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
new file mode 100644
index 00000000000..3d066843148
--- /dev/null
+++ b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.instrument' do
+ it 'is unsubscribeable' do
+ unsubscribe = described_class.instrument
+
+ expect(unsubscribe).not_to be_nil
+ expect { unsubscribe.call }.not_to raise_error
+ end
+ end
+
+ describe '#notify' do
+ subject { described_class.new }
+ let(:start) { Time.now }
+ let(:finish) { Time.now }
+
+ where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do
+ nil | "active_record:sqlquery" | nil | nil | nil | false | nil
+ "" | "active_record:sqlquery" | nil | nil | nil | false | nil
+ "User Load" | "active_record:User Load" | nil | nil | nil | false | nil
+ "Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil
+ nil | "active_record:sqlquery" | nil | 123 | nil | false | nil
+ nil | "active_record:sqlquery" | nil | nil | false | false | nil
+ nil | "active_record:sqlquery" | nil | nil | true | true | nil
+ nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users"
+ end
+
+ with_them do
+ def payload
+ {
+ name: name,
+ exception: exception,
+ connection_id: connection_id,
+ cached: cached,
+ sql: sql
+ }
+ end
+
+ def expected_tags
+ {
+ "component" => "ActiveRecord",
+ "span.kind" => "client",
+ "db.type" => "sql",
+ "db.connection_id" => connection_id,
+ "db.cached" => cached_response,
+ "db.statement" => sql
+ }
+ end
+
+ it 'should notify the tracer when the hash contains null values' do
+ expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
+
+ subject.notify(start, finish, payload)
+ end
+
+ it 'should notify the tracer when the payload is missing values' do
+ expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
+
+ subject.notify(start, finish, payload.compact)
+ end
+
+ it 'should not throw exceptions when with the default tracer' do
+ expect { subject.notify(start, finish, payload) }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 2a09f581f68..4f5993ba226 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -26,6 +26,8 @@ describe Gitlab::UsageData do
create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
+
+ ProjectFeature.first.update_attribute('repository_access_level', 0)
end
subject { described_class.data }
@@ -112,6 +114,7 @@ describe Gitlab::UsageData do
projects_slack_notifications_active
projects_slack_slash_active
projects_prometheus_active
+ projects_with_repositories_enabled
pages_domains
protected_branches
releases
@@ -134,6 +137,7 @@ describe Gitlab::UsageData do
expect(count_data[:projects_jira_cloud_active]).to eq(1)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
+ expect(count_data[:projects_with_repositories_enabled]).to eq(2)
expect(count_data[:clusters_enabled]).to eq(7)
expect(count_data[:project_clusters_enabled]).to eq(6)
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 6ac3d115bc6..5f7a0cca351 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -70,82 +70,6 @@ describe Gitlab do
end
end
- describe '.final_release?' do
- subject { described_class.final_release? }
-
- context 'returns the corrent boolean value' do
- it 'is false for a pre release' do
- stub_const('Gitlab::VERSION', '11.0.0-pre')
-
- expect(subject).to be false
- end
-
- it 'is false for a release candidate' do
- stub_const('Gitlab::VERSION', '11.0.0-rc2')
-
- expect(subject).to be false
- end
-
- it 'is true for a final release' do
- stub_const('Gitlab::VERSION', '11.0.2')
-
- expect(subject).to be true
- end
- end
- end
-
- describe '.minor_release' do
- subject { described_class.minor_release }
-
- it 'returns the minor release of the full GitLab version' do
- stub_const('Gitlab::VERSION', '11.0.1-rc3')
-
- expect(subject).to eql '11.0'
- end
- end
-
- describe '.previous_release' do
- subject { described_class.previous_release }
-
- context 'it should return the previous release' do
- it 'returns the previous major version when GitLab major version is not final' do
- stub_const('Gitlab::VERSION', '11.0.1-pre')
-
- expect(subject).to eql '10'
- end
-
- it 'returns the current minor version when the GitLab patch version is RC and > 0' do
- stub_const('Gitlab::VERSION', '11.2.1-rc3')
-
- expect(subject).to eql '11.2'
- end
-
- it 'returns the previous minor version when the GitLab patch version is RC and 0' do
- stub_const('Gitlab::VERSION', '11.2.0-rc3')
-
- expect(subject).to eql '11.1'
- end
- end
- end
-
- describe '.new_major_release?' do
- subject { described_class.new_major_release? }
-
- context 'returns the corrent boolean value' do
- it 'is true when the minor version is 0 and the patch is a pre release' do
- stub_const('Gitlab::VERSION', '11.0.1-pre')
-
- expect(subject).to be true
- end
-
- it 'is false when the minor version is above 0' do
- stub_const('Gitlab::VERSION', '11.2.1-rc3')
-
- expect(subject).to be false
- end
- end
- end
-
describe '.com?' do
it 'is true when on GitLab.com' do
stub_config_setting(url: 'https://gitlab.com')
diff --git a/spec/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb
new file mode 100644
index 00000000000..115e28c5994
--- /dev/null
+++ b/spec/lib/safe_zip/entry_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe SafeZip::Entry do
+ let(:target_path) { Dir.mktmpdir('safe-zip') }
+ let(:directories) { %w(public folder/with/subfolder) }
+ let(:params) { SafeZip::ExtractParams.new(directories: directories, to: target_path) }
+
+ let(:entry) { described_class.new(zip_archive, zip_entry, params) }
+ let(:entry_name) { 'public/folder/index.html' }
+ let(:entry_path_dir) { File.join(target_path, File.dirname(entry_name)) }
+ let(:entry_path) { File.join(target_path, entry_name) }
+ let(:zip_archive) { double }
+
+ let(:zip_entry) do
+ double(
+ name: entry_name,
+ file?: false,
+ directory?: false,
+ symlink?: false)
+ end
+
+ after do
+ FileUtils.remove_entry_secure(target_path)
+ end
+
+ context '#path_dir' do
+ subject { entry.path_dir }
+
+ it { is_expected.to eq(target_path + '/public/folder') }
+ end
+
+ context '#exist?' do
+ subject { entry.exist? }
+
+ context 'when entry does not exist' do
+ it { is_expected.not_to be_truthy }
+ end
+
+ context 'when entry does exist' do
+ before do
+ create_entry
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#extract' do
+ subject { entry.extract }
+
+ context 'when entry does not match the filtered directories' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:entry_name) do
+ [
+ 'assets/folder/index.html',
+ 'public/../folder/index.html',
+ 'public/../../../../../index.html',
+ '../../../../../public/index.html',
+ '/etc/passwd'
+ ]
+ end
+
+ with_them do
+ it 'does not extract file' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ context 'when entry does exist' do
+ before do
+ create_entry
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::AlreadyExistsError)
+ end
+ end
+
+ context 'when entry type is unknown' do
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::UnsupportedEntryError)
+ end
+ end
+
+ context 'when entry is valid' do
+ shared_examples 'secured symlinks' do
+ context 'when we try to extract entry into symlinked folder' do
+ before do
+ FileUtils.mkdir_p(File.join(target_path, "source"))
+ File.symlink("source", File.join(target_path, "public"))
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError)
+ end
+ end
+ end
+
+ context 'and is file' do
+ before do
+ allow(zip_entry).to receive(:file?) { true }
+ end
+
+ it 'does extract file' do
+ expect(zip_archive).to receive(:extract)
+ .with(zip_entry, entry_path)
+ .and_return(true)
+
+ is_expected.to be_truthy
+ end
+
+ it_behaves_like 'secured symlinks'
+ end
+
+ context 'and is directory' do
+ let(:entry_name) { 'public/folder/assets' }
+
+ before do
+ allow(zip_entry).to receive(:directory?) { true }
+ end
+
+ it 'does create directory' do
+ is_expected.to be_truthy
+
+ expect(File.exist?(entry_path)).to eq(true)
+ end
+
+ it_behaves_like 'secured symlinks'
+ end
+
+ context 'and is symlink' do
+ let(:entry_name) { 'public/folder/assets' }
+
+ before do
+ allow(zip_entry).to receive(:symlink?) { true }
+ allow(zip_archive).to receive(:read).with(zip_entry) { entry_symlink }
+ end
+
+ shared_examples 'a valid symlink' do
+ it 'does create symlink' do
+ is_expected.to be_truthy
+
+ expect(File.exist?(entry_path)).to eq(true)
+ end
+ end
+
+ context 'when source is within target' do
+ let(:entry_symlink) { '../images' }
+
+ context 'but does not exist' do
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::SymlinkSourceDoesNotExistError)
+ end
+ end
+
+ context 'and does exist' do
+ before do
+ FileUtils.mkdir_p(File.join(target_path, 'public', 'images'))
+ end
+
+ it_behaves_like 'a valid symlink'
+ end
+ end
+
+ context 'when source points outside of target' do
+ let(:entry_symlink) { '../../images' }
+
+ before do
+ FileUtils.mkdir(File.join(target_path, 'images'))
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError)
+ end
+ end
+
+ context 'when source points to /etc/passwd' do
+ let(:entry_symlink) { '/etc/passwd' }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError)
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def create_entry
+ FileUtils.mkdir_p(entry_path_dir)
+ FileUtils.touch(entry_path)
+ end
+end
diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb
new file mode 100644
index 00000000000..85e22cfa495
--- /dev/null
+++ b/spec/lib/safe_zip/extract_params_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe SafeZip::ExtractParams do
+ let(:target_path) { Dir.mktmpdir("safe-zip") }
+ let(:params) { described_class.new(directories: directories, to: target_path) }
+ let(:directories) { %w(public folder/with/subfolder) }
+
+ after do
+ FileUtils.remove_entry_secure(target_path)
+ end
+
+ describe '#extract_path' do
+ subject { params.extract_path }
+
+ it { is_expected.to eq(target_path) }
+ end
+
+ describe '#matching_target_directory' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { params.matching_target_directory(target_path + path) }
+
+ where(:path, :result) do
+ '/public/index.html' | '/public/'
+ '/non/existing/path' | nil
+ '/public' | nil
+ '/folder/with/index.html' | nil
+ end
+
+ with_them do
+ it { is_expected.to eq(result ? target_path + result : nil) }
+ end
+ end
+
+ describe '#target_directories' do
+ subject { params.target_directories }
+
+ it 'starts with target_path' do
+ is_expected.to all(start_with(target_path + '/'))
+ end
+
+ it 'ends with / for all paths' do
+ is_expected.to all(end_with('/'))
+ end
+ end
+
+ describe '#directories_wildcard' do
+ subject { params.directories_wildcard }
+
+ it 'adds * for all paths' do
+ is_expected.to all(end_with('/*'))
+ end
+ end
+end
diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb
new file mode 100644
index 00000000000..b75a8fede00
--- /dev/null
+++ b/spec/lib/safe_zip/extract_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe SafeZip::Extract do
+ let(:target_path) { Dir.mktmpdir('safe-zip') }
+ let(:directories) { %w(public) }
+ let(:object) { described_class.new(archive) }
+ let(:archive) { Rails.root.join('spec', 'fixtures', 'safe_zip', archive_name) }
+
+ after do
+ FileUtils.remove_entry_secure(target_path)
+ end
+
+ context '#extract' do
+ subject { object.extract(directories: directories, to: target_path) }
+
+ shared_examples 'extracts archive' do |param|
+ before do
+ stub_feature_flags(safezip_use_rubyzip: param)
+ end
+
+ it 'does extract archive' do
+ subject
+
+ expect(File.exist?(File.join(target_path, 'public', 'index.html'))).to eq(true)
+ expect(File.exist?(File.join(target_path, 'source'))).to eq(false)
+ end
+ end
+
+ shared_examples 'fails to extract archive' do |param|
+ before do
+ stub_feature_flags(safezip_use_rubyzip: param)
+ end
+
+ it 'does not extract archive' do
+ expect { subject }.to raise_error(SafeZip::Extract::Error)
+ end
+ end
+
+ %w(valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip).each do |name|
+ context "when using #{name} archive" do
+ let(:archive_name) { name }
+
+ context 'for RubyZip' do
+ it_behaves_like 'extracts archive', true
+ end
+
+ context 'for UnZip' do
+ it_behaves_like 'extracts archive', false
+ end
+ end
+ end
+
+ %w(invalid-symlink-does-not-exist.zip invalid-symlinks-outside.zip).each do |name|
+ context "when using #{name} archive" do
+ let(:archive_name) { name }
+
+ context 'for RubyZip' do
+ it_behaves_like 'fails to extract archive', true
+ end
+
+ context 'for UnZip (UNSAFE)' do
+ it_behaves_like 'extracts archive', false
+ end
+ end
+ end
+
+ context 'when no matching directories are found' do
+ let(:archive_name) { 'valid-simple.zip' }
+ let(:directories) { %w(non/existing) }
+
+ context 'for RubyZip' do
+ it_behaves_like 'fails to extract archive', true
+ end
+
+ context 'for UnZip' do
+ it_behaves_like 'fails to extract archive', false
+ end
+ end
+ end
+end
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index b36be0fd9c1..6fbf60a6222 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -3,30 +3,76 @@
require 'spec_helper'
describe Sentry::Client do
- let(:issue_status) { 'unresolved' }
- let(:limit) { 20 }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
- let(:sample_response) do
+ let(:issues_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(File.read(Rails.root.join('spec/fixtures/sentry/issues_sample_response.json')))
+ JSON.parse(fixture_file('sentry/issues_sample_response.json'))
+ )
+ end
+
+ let(:projects_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
)
end
subject(:client) { described_class.new(sentry_url, token) }
- describe '#list_issues' do
- subject { client.list_issues(issue_status: issue_status, limit: limit) }
+ # Requires sentry_api_url and subject to be defined
+ shared_examples 'no redirects' do
+ let(:redirect_to) { 'https://redirected.example.com' }
+ let(:other_url) { 'https://other.example.org' }
+
+ let!(:redirected_req_stub) { stub_sentry_request(other_url) }
+
+ let!(:redirect_req_stub) do
+ stub_sentry_request(
+ sentry_api_url,
+ status: 302,
+ headers: { location: redirect_to }
+ )
+ end
- before do
- stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sample_response)
+ it 'does not follow redirects' do
+ expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302')
+ expect(redirect_req_stub).to have_been_requested
+ expect(redirected_req_stub).not_to have_been_requested
end
+ end
- it 'returns objects of type ErrorTracking::Error' do
- expect(subject.length).to eq(1)
- expect(subject[0]).to be_a(Gitlab::ErrorTracking::Error)
+ shared_examples 'has correct return type' do |klass|
+ it "returns objects of type #{klass}" do
+ expect(subject).to all( be_a(klass) )
end
+ end
+
+ shared_examples 'has correct length' do |length|
+ it { expect(subject.length).to eq(length) }
+ end
+
+ # Requires sentry_api_request and subject to be defined
+ shared_examples 'calls sentry api' do
+ it 'calls sentry api' do
+ subject
+
+ expect(sentry_api_request).to have_been_requested
+ end
+ end
+
+ describe '#list_issues' do
+ let(:issue_status) { 'unresolved' }
+ let(:limit) { 20 }
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) }
+
+ subject { client.list_issues(issue_status: issue_status, limit: limit) }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'has correct length', 1
context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
@@ -50,7 +96,7 @@ describe Sentry::Client do
end
with_them do
- it { expect(subject[0].public_send(error_object)).to eq(sample_response[0].dig(*sentry_response)) }
+ it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) }
end
context 'external_url' do
@@ -61,24 +107,9 @@ describe Sentry::Client do
end
context 'redirects' do
- let(:redirect_to) { 'https://redirected.example.com' }
- let(:other_url) { 'https://other.example.org' }
-
- let!(:redirected_req_stub) { stub_sentry_request(other_url) }
-
- let!(:redirect_req_stub) do
- stub_sentry_request(
- sentry_url + '/issues/?limit=20&query=is:unresolved',
- status: 302,
- headers: { location: redirect_to }
- )
- end
+ let(:sentry_api_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
- it 'does not follow redirects' do
- expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302')
- expect(redirect_req_stub).to have_been_requested
- expect(redirected_req_stub).not_to have_been_requested
- end
+ it_behaves_like 'no redirects'
end
# Sentry API returns 404 if there are extra slashes in the URL!
@@ -99,7 +130,75 @@ describe Sentry::Client do
anything
).and_call_original
- client.list_issues(issue_status: issue_status, limit: limit)
+ subject
+
+ expect(valid_req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#list_projects' do
+ let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' }
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) }
+
+ subject { client.list_projects }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
+ it_behaves_like 'has correct length', 2
+
+ context 'keys missing in API response' do
+ it 'raises exception' do
+ projects_sample_response[0].delete(:slug)
+
+ stub_sentry_request(sentry_list_projects_url, body: projects_sample_response)
+
+ expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"')
+ end
+ end
+
+ context 'error object created from sentry response' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sentry_project_object, :sentry_response) do
+ :id | :id
+ :name | :name
+ :status | :status
+ :slug | :slug
+ :organization_name | [:organization, :name]
+ :organization_id | [:organization, :id]
+ :organization_slug | [:organization, :slug]
+ end
+
+ with_them do
+ it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) }
+ end
+ end
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_list_projects_url }
+
+ it_behaves_like 'no redirects'
+ end
+
+ # Sentry API returns 404 if there are extra slashes in the URL!
+ context 'extra slashes in URL' do
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api//0/projects//' }
+ let(:client) { described_class.new(sentry_url, token) }
+
+ let!(:valid_req_stub) do
+ stub_sentry_request(sentry_list_projects_url)
+ end
+
+ it 'removes extra slashes in api url' do
+ expect(Gitlab::HTTP).to receive(:get).with(
+ URI(sentry_list_projects_url),
+ anything
+ ).and_call_original
+
+ subject
expect(valid_req_stub).to have_been_requested
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1f5b4a8f908..4f578c48d5b 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -9,8 +9,10 @@ describe Notify do
include_context 'gitlab email notification'
+ let(:current_user_sanitized) { 'www_example_com' }
+
set(:user) { create(:user) }
- set(:current_user) { create(:user, email: "current@email.com") }
+ set(:current_user) { create(:user, email: "current@email.com", name: 'www.example.com') }
set(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
set(:merge_request) do
@@ -182,7 +184,7 @@ describe Notify do
aggregate_failures do
is_expected.to have_referable_subject(issue, reply: true)
is_expected.to have_body_text(status)
- is_expected.to have_body_text(current_user.name)
+ is_expected.to have_body_text(current_user_sanitized)
is_expected.to have_body_text(project_issue_path project, issue)
end
end
@@ -361,7 +363,7 @@ describe Notify do
aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
is_expected.to have_body_text(status)
- is_expected.to have_body_text(current_user.name)
+ is_expected.to have_body_text(current_user_sanitized)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
end
end
diff --git a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
new file mode 100644
index 00000000000..f8cf76cb339
--- /dev/null
+++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_migrator_sidekiq_queue.rb')
+
+describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do
+ include Gitlab::Database::MigrationHelpers
+
+ context 'when there are jobs in the queues' do
+ it 'correctly migrates queue when migrating up' do
+ Sidekiq::Testing.disable! do
+ stubbed_worker(queue: :storage_migrator).perform_async(1, 5)
+
+ described_class.new.up
+
+ expect(sidekiq_queue_length('storage_migrator')).to eq 0
+ expect(sidekiq_queue_length('hashed_storage:hashed_storage_migrator')).to eq 1
+ end
+ end
+
+ it 'correctly migrates queue when migrating down' do
+ Sidekiq::Testing.disable! do
+ stubbed_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5)
+
+ described_class.new.down
+
+ expect(sidekiq_queue_length('storage_migrator')).to eq 1
+ expect(sidekiq_queue_length('hashed_storage:hashed_storage_migrator')).to eq 0
+ end
+ end
+ end
+
+ context 'when there are no jobs in the queues' do
+ it 'does not raise error when migrating up' do
+ expect { described_class.new.up }.not_to raise_error
+ end
+
+ it 'does not raise error when migrating down' do
+ expect { described_class.new.down }.not_to raise_error
+ end
+ end
+
+ def stubbed_worker(queue:)
+ Class.new do
+ include Sidekiq::Worker
+ sidekiq_options queue: queue
+ end
+ end
+end
diff --git a/spec/migrations/update_project_import_visibility_level_spec.rb b/spec/migrations/update_project_import_visibility_level_spec.rb
new file mode 100644
index 00000000000..9ea9b956f67
--- /dev/null
+++ b/spec/migrations/update_project_import_visibility_level_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20181219130552_update_project_import_visibility_level.rb')
+
+describe UpdateProjectImportVisibilityLevel, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project) { projects.find_by_name(name) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ context 'private visibility level' do
+ let(:name) { 'private-public' }
+
+ it 'updates the project visibility' do
+ create_namespace(name, Gitlab::VisibilityLevel::PRIVATE)
+ create_project(name, Gitlab::VisibilityLevel::PUBLIC)
+
+ expect { migrate! }.to change { project.reload.visibility_level }.to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'internal visibility level' do
+ let(:name) { 'internal-public' }
+
+ it 'updates the project visibility' do
+ create_namespace(name, Gitlab::VisibilityLevel::INTERNAL)
+ create_project(name, Gitlab::VisibilityLevel::PUBLIC)
+
+ expect { migrate! }.to change { project.reload.visibility_level }.to(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'public visibility level' do
+ let(:name) { 'public-public' }
+
+ it 'does not update the project visibility' do
+ create_namespace(name, Gitlab::VisibilityLevel::PUBLIC)
+ create_project(name, Gitlab::VisibilityLevel::PUBLIC)
+
+ expect { migrate! }.not_to change { project.reload.visibility_level }
+ end
+ end
+
+ context 'private project visibility level' do
+ let(:name) { 'public-private' }
+
+ it 'does not update the project visibility' do
+ create_namespace(name, Gitlab::VisibilityLevel::PUBLIC)
+ create_project(name, Gitlab::VisibilityLevel::PRIVATE)
+
+ expect { migrate! }.not_to change { project.reload.visibility_level }
+ end
+ end
+
+ context 'no namespace' do
+ let(:name) { 'no-namespace' }
+
+ it 'does not update the project visibility' do
+ create_namespace(name, Gitlab::VisibilityLevel::PRIVATE, type: nil)
+ create_project(name, Gitlab::VisibilityLevel::PUBLIC)
+
+ expect { migrate! }.not_to change { project.reload.visibility_level }
+ end
+ end
+
+ def create_namespace(name, visibility, options = {})
+ namespaces.create({
+ name: name,
+ path: name,
+ type: 'Group',
+ visibility_level: visibility
+ }.merge(options))
+ end
+
+ def create_project(name, visibility)
+ projects.create!(namespace_id: namespaces.find_by_name(name).id,
+ name: name,
+ path: name,
+ import_type: 'gitlab_project',
+ visibility_level: visibility)
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 199f49d0bf2..eee80e9bad7 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -298,7 +298,6 @@ describe Ability do
context 'wiki named abilities' do
it 'disables wiki abilities if the project has no wiki' do
- expect(project).to receive(:has_external_wiki?).and_return(false)
expect(subject).not_to be_allowed(:read_wiki)
expect(subject).not_to be_allowed(:create_wiki)
expect(subject).not_to be_allowed(:update_wiki)
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
new file mode 100644
index 00000000000..68aed387bfc
--- /dev/null
+++ b/spec/models/application_record_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ApplicationRecord do
+ describe '#id_in' do
+ let(:records) { create_list(:user, 3) }
+
+ it 'returns records of the ids' do
+ expect(User.id_in(records.last(2).map(&:id))).to eq(records.last(2))
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 8ba33ff9c04..8a1bbb26e57 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2133,6 +2133,8 @@ describe Ci::Build do
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
+ { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true },
+ { key: 'CI_PAGES_URL', value: project.pages_url, public: true },
{ key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true },
{ key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true },
{ key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 17f33785fda..72a0df96a80 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -39,6 +39,29 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.processables' do
+ before do
+ create(:ci_build, name: 'build', pipeline: pipeline)
+ create(:ci_bridge, name: 'bridge', pipeline: pipeline)
+ create(:commit_status, name: 'commit status', pipeline: pipeline)
+ create(:generic_commit_status, name: 'generic status', pipeline: pipeline)
+ end
+
+ it 'has an association with processable CI/CD entities' do
+ pipeline.processables.pluck('name').yield_self do |processables|
+ expect(processables).to match_array %w[build bridge]
+ end
+ end
+
+ it 'makes it possible to append a new processable' do
+ pipeline.processables << build(:ci_bridge)
+
+ pipeline.save!
+
+ expect(pipeline.processables.reload.count).to eq 3
+ end
+ end
+
describe '.sort_by_merge_request_pipelines' do
subject { described_class.sort_by_merge_request_pipelines }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index a2d2d77746d..baad8352185 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -11,6 +11,7 @@ describe Commit do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(StaticModel) }
+ it { is_expected.to include_module(Presentable) }
end
describe '.lazy' do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index f8d50e89d40..ef6af232999 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -67,13 +67,17 @@ describe CacheMarkdownField do
end
let(:markdown) { '`Foo`' }
- let(:html) { '<p dir="auto"><code>Foo</code></p>' }
+ let(:html) { '<p dir="auto"><code>Foo</code></p>' }
let(:updated_markdown) { '`Bar`' }
- let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
+ let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ before do
+ stub_commonmark_sourcepos_disabled
+ end
+
describe '.attributes' do
it 'excludes cache attributes' do
expect(thing.attributes.keys.sort).to eq(%w[bar baz foo])
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index 689e7d3058f..43a544cfe26 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -159,6 +159,10 @@ describe CacheableAttributes do
describe 'edge cases' do
describe 'caching behavior', :use_clean_rails_memory_store_caching do
+ before do
+ stub_commonmark_sourcepos_disabled
+ end
+
it 'retrieves upload fields properly' do
ar_record = create(:appearance, :with_logo)
ar_record.cache!
diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb
index 7d320edd492..7feeaa54069 100644
--- a/spec/models/concerns/redactable_spec.rb
+++ b/spec/models/concerns/redactable_spec.rb
@@ -1,6 +1,10 @@
require 'spec_helper'
describe Redactable do
+ before do
+ stub_commonmark_sourcepos_disabled
+ end
+
shared_examples 'model with redactable field' do
it 'redacts unsubscribe token' do
model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 62699df5611..f93904065c7 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -91,6 +91,12 @@ describe GlobalMilestone do
it 'sorts collection by due date' do
expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil]
end
+
+ it 'filters milestones by search_title when params[:search_title] is present' do
+ global_milestones = described_class.build_collection(projects, { search_title: 'v1.2' })
+
+ expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2'])
+ end
end
context 'when adding new milestones' do
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index a5ce245c21d..e1a7a59dfd1 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -10,6 +10,40 @@ describe Identity do
it { is_expected.to respond_to(:extern_uid) }
end
+ describe 'validations' do
+ set(:user) { create(:user) }
+
+ context 'with existing user and provider' do
+ before do
+ create(:identity, provider: 'ldapmain', user_id: user.id)
+ end
+
+ it 'returns false for a duplicate entry' do
+ identity = user.identities.build(provider: 'ldapmain', user_id: user.id)
+
+ expect(identity.validate).to be_falsey
+ end
+
+ it 'returns true when a different provider is used' do
+ identity = user.identities.build(provider: 'gitlab', user_id: user.id)
+
+ expect(identity.validate).to be_truthy
+ end
+ end
+
+ context 'with newly-created user' do
+ before do
+ create(:identity, provider: 'ldapmain', user_id: nil)
+ end
+
+ it 'successfully validates even with a nil user_id' do
+ identity = user.identities.build(provider: 'ldapmain')
+
+ expect(identity.validate).to be_truthy
+ end
+ end
+ end
+
describe '#is_ldap?' do
let(:ldap_identity) { create(:identity, provider: 'ldapmain') }
let(:other_identity) { create(:identity, provider: 'twitter') }
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 4696341c05f..d32f163f05b 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -13,6 +13,29 @@ describe InternalId do
it { is_expected.to validate_presence_of(:usage) }
end
+ describe '.flush_records!' do
+ subject { described_class.flush_records!(project: project) }
+
+ let(:another_project) { create(:project) }
+
+ before do
+ create_list(:issue, 2, project: project)
+ create_list(:issue, 2, project: another_project)
+ end
+
+ it 'deletes all records for the given project' do
+ expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0)
+ end
+
+ it 'retains records for other projects' do
+ expect { subject }.not_to change { described_class.where(project: another_project).count }
+ end
+
+ it 'does not allow an empty filter' do
+ expect { described_class.flush_records!({}) }.to raise_error(/filter cannot be empty/)
+ end
+ end
+
describe '.generate_next' do
subject { described_class.generate_next(issue, scope, usage, init) }
diff --git a/spec/models/lfs_download_object_spec.rb b/spec/models/lfs_download_object_spec.rb
new file mode 100644
index 00000000000..88838b127d2
--- /dev/null
+++ b/spec/models/lfs_download_object_spec.rb
@@ -0,0 +1,68 @@
+require 'rails_helper'
+
+describe LfsDownloadObject do
+ let(:oid) { 'cd293be6cea034bd45a0352775a219ef5dc7825ce55d1f7dae9762d80ce64411' }
+ let(:link) { 'http://www.example.com' }
+ let(:size) { 1 }
+
+ subject { described_class.new(oid: oid, size: size, link: link) }
+
+ describe 'validations' do
+ it { is_expected.to validate_numericality_of(:size).is_greater_than_or_equal_to(0) }
+
+ context 'oid attribute' do
+ it 'must be 64 characters long' do
+ aggregate_failures do
+ expect(described_class.new(oid: 'a' * 63, size: size, link: link)).to be_invalid
+ expect(described_class.new(oid: 'a' * 65, size: size, link: link)).to be_invalid
+ expect(described_class.new(oid: 'a' * 64, size: size, link: link)).to be_valid
+ end
+ end
+
+ it 'must contain only hexadecimal characters' do
+ aggregate_failures do
+ expect(subject).to be_valid
+ expect(described_class.new(oid: 'g' * 64, size: size, link: link)).to be_invalid
+ end
+ end
+ end
+
+ context 'link attribute' do
+ it 'only http and https protocols are valid' do
+ aggregate_failures do
+ expect(described_class.new(oid: oid, size: size, link: 'http://www.example.com')).to be_valid
+ expect(described_class.new(oid: oid, size: size, link: 'https://www.example.com')).to be_valid
+ expect(described_class.new(oid: oid, size: size, link: 'ftp://www.example.com')).to be_invalid
+ expect(described_class.new(oid: oid, size: size, link: 'ssh://www.example.com')).to be_invalid
+ expect(described_class.new(oid: oid, size: size, link: 'git://www.example.com')).to be_invalid
+ end
+ end
+
+ it 'cannot be empty' do
+ expect(described_class.new(oid: oid, size: size, link: '')).not_to be_valid
+ end
+
+ context 'when localhost or local network addresses' do
+ subject { described_class.new(oid: oid, size: size, link: 'http://192.168.1.1') }
+
+ before do
+ allow(ApplicationSetting)
+ .to receive(:current)
+ .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: setting))
+ end
+
+ context 'are allowed' do
+ let(:setting) { true }
+
+ it { expect(subject).to be_valid }
+ end
+
+ context 'are not allowed' do
+ let(:setting) { false }
+
+ it { expect(subject).to be_invalid }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 2e436f2cc8a..af7e3d3a6c9 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -240,6 +240,29 @@ describe Milestone do
end
end
+ describe '#search_title' do
+ let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
+
+ it 'returns milestones with a matching title' do
+ expect(described_class.search_title(milestone.title)) .to eq([milestone])
+ end
+
+ it 'returns milestones with a partially matching title' do
+ expect(described_class.search_title(milestone.title[0..2])).to eq([milestone])
+ end
+
+ it 'returns milestones with a matching title regardless of the casing' do
+ expect(described_class.search_title(milestone.title.upcase))
+ .to eq([milestone])
+ end
+
+ it 'searches only on the title and ignores milestones with a matching description' do
+ create(:milestone, title: 'bar', description: 'foo')
+
+ expect(described_class.search_title(milestone.title)) .to eq([milestone])
+ end
+ end
+
describe '#for_projects_and_groups' do
let(:project) { create(:project) }
let(:project_other) { create(:project) }
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index ee84fa95f0e..b880d90d28f 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -144,7 +144,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
end
- describe '#calculate_reactive_cache' do
+ shared_examples 'reactive cache calculation' do
context '#build_page' do
subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
@@ -155,7 +155,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
it 'returns a specific URL when response has no results' do
- stub_request(body: bamboo_response(size: 0))
+ stub_request(body: %q({"results":{"results":{"size":"0"}}}))
is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
end
@@ -224,6 +224,24 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
end
+ describe '#calculate_reactive_cache' do
+ context 'when Bamboo API returns single result' do
+ let(:bamboo_response_template) do
+ %q({"results":{"results":{"size":"1","result":{"buildState":"%{build_state}","planResultKey":{"key":"42"}}}}})
+ end
+
+ it_behaves_like 'reactive cache calculation'
+ end
+
+ context 'when Bamboo API returns an array of results and we only consider the last one' do
+ let(:bamboo_response_template) do
+ %q({"results":{"results":{"size":"2","result":[{"buildState":"%{build_state}","planResultKey":{"key":"41"}},{"buildState":"%{build_state}","planResultKey":{"key":"42"}}]}}})
+ end
+
+ it_behaves_like 'reactive cache calculation'
+ end
+ end
+
def stub_update_and_build_request(status: 200, body: nil)
bamboo_full_url = 'http://gitlab.com/bamboo/updateAndBuild.action?buildKey=foo&os_authType=basic'
@@ -244,8 +262,8 @@ describe BambooService, :use_clean_rails_memory_store_caching do
).with(basic_auth: %w(mic password))
end
- def bamboo_response(result_key: 42, build_state: 'success', size: 1)
+ def bamboo_response(build_state: 'success')
# reference: https://docs.atlassian.com/atlassian-bamboo/REST/6.2.5/#d2e786
- %Q({"results":{"results":{"size":"#{size}","result":[{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}]}}})
+ bamboo_response_template % { build_state: build_state }
end
end
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 25e6ce7e804..62fd97b038b 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -1,7 +1,6 @@
require 'spec_helper'
describe ExternalWikiService do
- include ExternalWikiHelper
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -25,24 +24,4 @@ describe ExternalWikiService do
it { is_expected.not_to validate_presence_of(:external_wiki_url) }
end
end
-
- describe 'External wiki' do
- let(:project) { create(:project) }
-
- context 'when it is active' do
- before do
- properties = { 'external_wiki_url' => 'https://gitlab.com' }
- @service = project.create_external_wiki_service(active: true, properties: properties)
- end
-
- after do
- @service.destroy!
- end
-
- it 'replaces the wiki url' do
- wiki_path = get_project_wiki_path(project)
- expect(wiki_path).to match('https://gitlab.com')
- end
- end
- end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 437f0066450..ae137aa7b78 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -405,6 +405,30 @@ describe Project do
end
end
+ describe '#all_pipelines' do
+ let(:project) { create(:project) }
+
+ before do
+ create(:ci_pipeline, project: project, ref: 'master', source: :web)
+ create(:ci_pipeline, project: project, ref: 'master', source: :external)
+ end
+
+ it 'has all pipelines' do
+ expect(project.all_pipelines.size).to eq(2)
+ end
+
+ context 'when builds are disabled' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ it 'should return .external pipelines' do
+ expect(project.all_pipelines).to all(have_attributes(source: 'external'))
+ expect(project.all_pipelines.size).to eq(1)
+ end
+ end
+ end
+
describe 'project token' do
it 'sets an random token if none provided' do
project = FactoryBot.create(:project, runners_token: '')
@@ -3074,6 +3098,66 @@ describe Project do
end
end
+ describe '.with_feature_available_for_user' do
+ let!(:user) { create(:user) }
+ let!(:feature) { MergeRequest }
+ let!(:project) { create(:project, :public, :merge_requests_enabled) }
+
+ subject { described_class.with_feature_available_for_user(feature, user) }
+
+ context 'when user has access to project' do
+ subject { described_class.with_feature_available_for_user(feature, user) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ context 'when public project' do
+ context 'when feature is public' do
+ it 'returns project' do
+ is_expected.to include(project)
+ end
+ end
+
+ context 'when feature is private' do
+ let!(:project) { create(:project, :public, :merge_requests_private) }
+
+ it 'returns project when user has access to the feature' do
+ project.add_maintainer(user)
+
+ is_expected.to include(project)
+ end
+
+ it 'does not return project when user does not have the minimum access level required' do
+ is_expected.not_to include(project)
+ end
+ end
+ end
+
+ context 'when private project' do
+ let!(:project) { create(:project) }
+
+ it 'returns project when user has access to the feature' do
+ project.add_maintainer(user)
+
+ is_expected.to include(project)
+ end
+
+ it 'does not return project when user does not have the minimum access level required' do
+ is_expected.not_to include(project)
+ end
+ end
+ end
+
+ context 'when user does not have access to project' do
+ let!(:project) { create(:project) }
+
+ it 'does not return project when user cant access project' do
+ is_expected.not_to include(project)
+ end
+ end
+ end
+
describe '#pages_available?' do
let(:project) { create(:project, group: group) }
@@ -3224,7 +3308,7 @@ describe Project do
end
context 'legacy storage' do
- let(:project) { create(:project, :repository, :legacy_storage) }
+ set(:project) { create(:project, :repository, :legacy_storage) }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project_storage) { project.send(:storage) }
@@ -3279,13 +3363,14 @@ describe Project do
end
describe '#migrate_to_hashed_storage!' do
+ let(:project) { create(:project, :empty_repo, :legacy_storage) }
+
it 'returns true' do
expect(project.migrate_to_hashed_storage!).to be_truthy
end
- it 'does not validate project visibility' do
- expect(project).not_to receive(:visibility_level_allowed_as_fork)
- expect(project).not_to receive(:visibility_level_allowed_by_group)
+ it 'does not run validation' do
+ expect(project).not_to receive(:valid?)
project.migrate_to_hashed_storage!
end
@@ -3315,7 +3400,7 @@ describe Project do
end
context 'hashed storage' do
- let(:project) { create(:project, :repository, skip_disk_validation: true) }
+ set(:project) { create(:project, :repository, skip_disk_validation: true) }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) }
let(:hashed_prefix) { File.join('@hashed', hash[0..1], hash[2..3]) }
@@ -3372,6 +3457,8 @@ describe Project do
end
describe '#migrate_to_hashed_storage!' do
+ let(:project) { create(:project, :repository, skip_disk_validation: true) }
+
it 'returns nil' do
expect(project.migrate_to_hashed_storage!).to be_nil
end
@@ -3381,10 +3468,12 @@ describe Project do
end
context 'when partially migrated' do
- it 'returns true' do
+ it 'enqueues a job' do
project = create(:project, storage_version: 1, skip_disk_validation: true)
- expect(project.migrate_to_hashed_storage!).to be_truthy
+ Sidekiq::Testing.fake! do
+ expect { project.migrate_to_hashed_storage! }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
+ end
end
end
end
@@ -3762,6 +3851,7 @@ describe Project do
expect(import_state).to receive(:remove_jid)
expect(project).to receive(:after_create_default_branch)
expect(project).to receive(:refresh_markdown_cache!)
+ expect(InternalId).to receive(:flush_records!).with(project: project)
project.after_import
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index c4af17f4726..3537dead5d1 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -178,6 +178,21 @@ describe ProjectTeam do
end
end
+ describe '#members_in_project_and_ancestors' do
+ context 'group project' do
+ it 'filters out users who are not members of the project' do
+ group = create(:group)
+ project = create(:project, group: group)
+ group_member = create(:group_member, group: group)
+ old_user = create(:user)
+
+ ProjectAuthorization.create!(project: project, user: old_user, access_level: Gitlab::Access::GUEST)
+
+ expect(project.team.members_in_project_and_ancestors).to contain_exactly(group_member.user)
+ end
+ end
+ end
+
describe "#human_max_access" do
it 'returns Maintainer role' do
user = create(:user)
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 5ec04b99957..677613b7980 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -48,7 +48,7 @@ describe SentNotification do
let(:note) { create(:diff_note_on_merge_request) }
it 'creates a new SentNotification' do
- expect { described_class.record_note(note, user.id) }.to change { described_class.count }.by(1)
+ expect { described_class.record_note(note, note.author.id) }.to change { described_class.count }.by(1)
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index 2898613545c..b2ef17a81d4 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
describe UserPreference do
+ let(:user_preference) { create(:user_preference) }
+
describe '#set_notes_filter' do
let(:issuable) { build_stubbed(:issue) }
- let(:user_preference) { create(:user_preference) }
shared_examples 'setting system notes' do
it 'returns updated discussion filter' do
@@ -50,4 +51,26 @@ describe UserPreference do
end
end
end
+
+ describe 'sort_by preferences' do
+ shared_examples_for 'a sort_by preference' do
+ it 'allows nil sort fields' do
+ user_preference.update(attribute => nil)
+
+ expect(user_preference).to be_valid
+ end
+ end
+
+ context 'merge_requests_sort attribute' do
+ let(:attribute) { :merge_requests_sort }
+
+ it_behaves_like 'a sort_by preference'
+ end
+
+ context 'issues_sort attribute' do
+ let(:attribute) { :issues_sort }
+
+ it_behaves_like 'a sort_by preference'
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 33842e74b92..78477ab0a5a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1997,6 +1997,33 @@ describe User do
expect(subject).to include(accessible)
expect(subject).not_to include(other)
end
+
+ context 'with min_access_level' do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project, :private, namespace: user.namespace) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ subject { Project.where("EXISTS (?)", user.authorizations_for_projects(min_access_level: min_access_level)) }
+
+ context 'when developer access' do
+ let(:min_access_level) { Gitlab::Access::DEVELOPER }
+
+ it 'includes projects a user has access to' do
+ expect(subject).to include(project)
+ end
+ end
+
+ context 'when owner access' do
+ let(:min_access_level) { Gitlab::Access::OWNER }
+
+ it 'does not include projects with higher access level' do
+ expect(subject).not_to include(project)
+ end
+ end
+ end
end
describe '#authorized_projects', :delete do
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 8022f61e67d..844d96017de 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -75,6 +75,14 @@ describe Ci::PipelinePolicy, :models do
end
end
+ context 'when user does not have access to internal CI' do
+ let(:project) { create(:project, :builds_disabled, :public) }
+
+ it 'disallows the user from reading the pipeline' do
+ expect(policy).to be_disallowed :read_pipeline
+ end
+ end
+
describe 'destroy_pipeline' do
let(:project) { create(:project, :public) }
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index 7e25c53e77c..0e848c74659 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -28,6 +28,7 @@ describe NotePolicy, mdoels: true do
expect(policy).to be_disallowed(:admin_note)
expect(policy).to be_disallowed(:resolve_note)
expect(policy).to be_disallowed(:read_note)
+ expect(policy).to be_disallowed(:award_emoji)
end
end
@@ -40,6 +41,7 @@ describe NotePolicy, mdoels: true do
expect(policy).to be_allowed(:admin_note)
expect(policy).to be_allowed(:resolve_note)
expect(policy).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:award_emoji)
end
end
end
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index 397eaee068c..a38e0dbd797 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -14,6 +14,13 @@ describe PersonalSnippetPolicy do
]
end
+ let(:comment_permissions) do
+ [
+ :comment_personal_snippet,
+ :create_note
+ ]
+ end
+
def permissions(user)
described_class.new(user, snippet)
end
@@ -26,7 +33,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -37,7 +44,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(*comment_permissions)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -48,7 +55,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(*comment_permissions)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -63,7 +70,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -74,7 +81,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(*comment_permissions)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -85,7 +92,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -96,7 +103,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(*comment_permissions)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -111,7 +118,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -122,7 +129,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -144,7 +151,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(*comment_permissions)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -155,7 +162,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(:comment_personal_snippet)
+ is_expected.to be_allowed(*comment_permissions)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 7705704a07f..93a468f585b 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -12,7 +12,7 @@ describe ProjectPolicy do
let(:base_guest_permissions) do
%i[
read_project read_board read_list read_wiki read_issue
- read_project_for_iids read_issue_iid read_merge_request_iid read_label
+ read_project_for_iids read_issue_iid read_label
read_milestone read_project_snippet read_project_member read_note
create_project create_issue create_note upload_file create_merge_request_in
award_emoji read_release
@@ -102,15 +102,27 @@ describe ProjectPolicy do
expect(Ability).not_to be_allowed(user, :read_issue, project)
end
- context 'when the feature is disabled' do
+ context 'wiki feature' do
+ let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
+
subject { described_class.new(owner, project) }
- before do
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
- end
+ context 'when the feature is disabled' do
+ before do
+ project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ end
- it 'does not include the wiki permissions' do
- expect_disallowed :read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code
+ it 'does not include the wiki permissions' do
+ expect_disallowed(*permissions)
+ end
+
+ context 'when there is an external wiki' do
+ it 'does not include the wiki permissions' do
+ allow(project).to receive(:has_external_wiki?).and_return(true)
+
+ expect_disallowed(*permissions)
+ end
+ end
end
end
@@ -152,22 +164,52 @@ describe ProjectPolicy do
end
end
+ context 'for a guest in a private project' do
+ let(:project) { create(:project, :private) }
+ subject { described_class.new(guest, project) }
+
+ it 'disallows the guest from reading the merge request and merge request iid' do
+ expect_disallowed(:read_merge_request)
+ expect_disallowed(:read_merge_request_iid)
+ end
+ end
+
context 'builds feature' do
- subject { described_class.new(owner, project) }
+ context 'when builds are disabled' do
+ subject { described_class.new(owner, project) }
- it 'disallows all permissions when the feature is disabled' do
- project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
+ before do
+ project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
+ end
- builds_permissions = [
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
- :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
- :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
- :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
- ]
+ it 'disallows all permissions except pipeline when the feature is disabled' do
+ builds_permissions = [
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+ ]
- expect_disallowed(*builds_permissions)
+ expect_disallowed(*builds_permissions)
+ end
+ end
+
+ context 'when builds are disabled only for some users' do
+ subject { described_class.new(guest, project) }
+
+ before do
+ project.project_feature.update(builds_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'disallows pipeline and commit_status permissions' do
+ builds_permissions = [
+ :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_commit_status, :update_commit_status, :admin_commit_status, :destroy_commit_status
+ ]
+
+ expect_disallowed(*builds_permissions)
+ end
end
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index 4d32e06b553..d6329e84579 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -41,7 +41,7 @@ describe ProjectSnippetPolicy do
subject { abilities(regular_user, :public) }
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -50,7 +50,7 @@ describe ProjectSnippetPolicy do
subject { abilities(external_user, :public) }
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -70,7 +70,7 @@ describe ProjectSnippetPolicy do
subject { abilities(regular_user, :internal) }
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -79,7 +79,7 @@ describe ProjectSnippetPolicy do
subject { abilities(external_user, :internal) }
it do
- expect_disallowed(:read_project_snippet)
+ expect_disallowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -92,7 +92,7 @@ describe ProjectSnippetPolicy do
end
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -112,7 +112,7 @@ describe ProjectSnippetPolicy do
subject { abilities(regular_user, :private) }
it do
- expect_disallowed(:read_project_snippet)
+ expect_disallowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -123,7 +123,7 @@ describe ProjectSnippetPolicy do
subject { described_class.new(regular_user, snippet) }
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_allowed(*author_permissions)
end
end
@@ -136,7 +136,7 @@ describe ProjectSnippetPolicy do
end
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -149,7 +149,7 @@ describe ProjectSnippetPolicy do
end
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
end
@@ -158,7 +158,7 @@ describe ProjectSnippetPolicy do
subject { abilities(create(:admin), :private) }
it do
- expect_allowed(:read_project_snippet)
+ expect_allowed(:read_project_snippet, :create_note)
expect_allowed(*author_permissions)
end
end
diff --git a/spec/presenters/ci/trigger_presenter_spec.rb b/spec/presenters/ci/trigger_presenter_spec.rb
new file mode 100644
index 00000000000..231b539c188
--- /dev/null
+++ b/spec/presenters/ci/trigger_presenter_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Ci::TriggerPresenter do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
+ set(:trigger) do
+ create(:ci_trigger, token: '123456789abcd', project: project)
+ end
+
+ subject do
+ described_class.new(trigger, current_user: user)
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when user is not a trigger owner' do
+ describe '#token' do
+ it 'exposes only short token' do
+ expect(subject.token).not_to eq trigger.token
+ expect(subject.token).to eq '1234'
+ end
+ end
+
+ describe '#has_token_exposed?' do
+ it 'does not have token exposed' do
+ expect(subject).not_to have_token_exposed
+ end
+ end
+ end
+
+ context 'when user is a trigger owner and builds admin' do
+ before do
+ trigger.update(owner: user)
+ end
+
+ describe '#token' do
+ it 'exposes full token' do
+ expect(subject.token).to eq trigger.token
+ end
+ end
+
+ describe '#has_token_exposed?' do
+ it 'has token exposed' do
+ expect(subject).to have_token_exposed
+ end
+ end
+ end
+end
diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb
new file mode 100644
index 00000000000..4a0d3a28c32
--- /dev/null
+++ b/spec/presenters/commit_presenter_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CommitPresenter do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
+ let(:user) { create(:user) }
+ let(:presenter) { described_class.new(commit, current_user: user) }
+
+ describe '#status_for' do
+ subject { presenter.status_for('ref') }
+
+ context 'when user can read_commit_status' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(true)
+ end
+
+ it 'returns commit status for ref' do
+ expect(commit).to receive(:status).with('ref').and_return('test')
+
+ expect(subject).to eq('test')
+ end
+ end
+
+ context 'when user can not read_commit_status' do
+ it 'is false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+
+ describe '#any_pipelines?' do
+ subject { presenter.any_pipelines? }
+
+ context 'when user can read pipeline' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :read_pipeline, project).and_return(true)
+ end
+
+ it 'returns if there are any pipelines for commit' do
+ expect(commit).to receive_message_chain(:pipelines, :any?).and_return(true)
+
+ expect(subject).to eq(true)
+ end
+ end
+
+ context 'when user can not read pipeline' do
+ it 'is false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/container_registry_spec.rb b/spec/requests/api/container_registry_spec.rb
new file mode 100644
index 00000000000..ea035a8be4a
--- /dev/null
+++ b/spec/requests/api/container_registry_spec.rb
@@ -0,0 +1,224 @@
+require 'spec_helper'
+
+describe API::ContainerRegistry do
+ set(:project) { create(:project, :private) }
+ set(:maintainer) { create(:user) }
+ set(:developer) { create(:user) }
+ set(:reporter) { create(:user) }
+ set(:guest) { create(:user) }
+
+ let(:root_repository) { create(:container_repository, :root, project: project) }
+ let(:test_repository) { create(:container_repository, project: project) }
+
+ let(:api_user) { maintainer }
+
+ before do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+
+ stub_feature_flags(container_registry_api: true)
+ stub_container_registry_config(enabled: true)
+
+ root_repository
+ test_repository
+ end
+
+ shared_examples 'being disallowed' do |param|
+ context "for #{param}" do
+ let(:api_user) { public_send(param) }
+
+ it 'returns access denied' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context "for anonymous" do
+ let(:api_user) { nil }
+
+ it 'returns not found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/registry/repositories' do
+ subject { get api("/projects/#{project.id}/registry/repositories", api_user) }
+
+ it_behaves_like 'being disallowed', :guest
+
+ context 'for reporter' do
+ let(:api_user) { reporter }
+
+ it 'returns a list of repositories' do
+ subject
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |repository| repository['id'] }).to contain_exactly(
+ root_repository.id, test_repository.id)
+ end
+
+ it 'returns a matching schema' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/repositories')
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id' do
+ subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}", api_user) }
+
+ it_behaves_like 'being disallowed', :developer
+
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
+
+ it 'schedules removal of repository' do
+ expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do
+ subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user) }
+
+ it_behaves_like 'being disallowed', :guest
+
+ context 'for reporter' do
+ let(:api_user) { reporter }
+
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
+ end
+
+ it 'returns a list of tags' do
+ subject
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA)
+ end
+
+ it 'returns a matching schema' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tags')
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do
+ subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user), params: params }
+
+ it_behaves_like 'being disallowed', :developer do
+ let(:params) do
+ { name_regex: 'v10.*' }
+ end
+ end
+
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
+
+ context 'without required parameters' do
+ let(:params) { }
+
+ it 'returns bad request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'passes all declared parameters' do
+ let(:params) do
+ { name_regex: 'v10.*',
+ keep_n: 100,
+ older_than: '1 day',
+ other: 'some value' }
+ end
+
+ let(:worker_params) do
+ { name_regex: 'v10.*',
+ keep_n: 100,
+ older_than: '1 day' }
+ end
+
+ it 'schedules cleanup of tags repository' do
+ expect(CleanupContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id, worker_params)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
+ subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+
+ it_behaves_like 'being disallowed', :guest
+
+ context 'for reporter' do
+ let(:api_user) { reporter }
+
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
+
+ it 'returns a details of tag' do
+ subject
+
+ expect(json_response).to include(
+ 'name' => 'rootA',
+ 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15',
+ 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac',
+ 'total_size' => 2319870)
+ end
+
+ it 'returns a matching schema' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/tag')
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do
+ subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) }
+
+ it_behaves_like 'being disallowed', :developer
+
+ context 'for maintainer' do
+ let(:api_user) { maintainer }
+
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true)
+ end
+
+ it 'properly removes tag' do
+ expect_any_instance_of(ContainerRegistry::Client)
+ .to receive(:delete_repository_tag).with(root_repository.path,
+ 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15')
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index e82ef002d32..0cf5c5677b9 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -7,6 +7,8 @@ describe API::Markdown do
let(:user) {} # No-op. It gets overwritten in the contexts below.
before do
+ stub_commonmark_sourcepos_disabled
+
post api("/markdown", user), params: params
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 15dc901d06e..f0f01e97f1d 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe API::Triggers do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+
let!(:trigger_token) { 'secure_token' }
let!(:trigger_token_2) { 'secure_token_2' }
let!(:project) { create(:project, :repository, creator: user) }
@@ -132,14 +133,17 @@ describe API::Triggers do
end
describe 'GET /projects/:id/triggers' do
- context 'authenticated user with valid permissions' do
- it 'returns list of triggers' do
+ context 'authenticated user who can access triggers' do
+ it 'returns a list of triggers with tokens exposed correctly' do
get api("/projects/#{project.id}/triggers", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
+
expect(json_response).to be_a(Array)
- expect(json_response[0]).to have_key('token')
+ expect(json_response.size).to eq 2
+ expect(json_response.dig(0, 'token')).to eq trigger_token
+ expect(json_response.dig(1, 'token')).to eq trigger_token_2[0..3]
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index f1514e90eb2..1781759c54b 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1086,6 +1086,12 @@ describe 'Git LFS API and storage' do
end
end
+ context 'and request to finalize the upload is not sent by gitlab-workhorse' do
+ it 'fails with a JWT decode error' do
+ expect { put_finalize(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError)
+ end
+ end
+
context 'and workhorse requests upload finalize for a new lfs object' do
before do
lfs_object.destroy
@@ -1347,9 +1353,13 @@ describe 'Git LFS API and storage' do
context 'when pushing the same lfs object to the second project' do
before do
+ finalize_headers = headers
+ .merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
+ .merge(workhorse_internal_api_request_header)
+
put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}",
params: {},
- headers: headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file).compact
+ headers: finalize_headers
end
it 'responds with status 200' do
@@ -1370,7 +1380,7 @@ describe 'Git LFS API and storage' do
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", params: {}, headers: authorize_headers
end
- def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, args: {})
+ def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, args: {})
upload_path = LfsObjectUploader.workhorse_local_upload_path
file_path = upload_path + '/' + lfs_tmp if lfs_tmp
@@ -1384,11 +1394,14 @@ describe 'Git LFS API and storage' do
'file.name' => File.basename(file_path)
}
- put_finalize_with_args(args.merge(extra_args).compact)
+ put_finalize_with_args(args.merge(extra_args).compact, verified: verified)
end
- def put_finalize_with_args(args)
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: headers
+ def put_finalize_with_args(args, verified:)
+ finalize_headers = headers
+ finalize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: finalize_headers
end
def lfs_tmp_file
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 852b6af9f7f..88d16a5b360 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ClusterApplicationEntity do
describe '#as_json' do
- let(:application) { build(:clusters_applications_helm) }
+ let(:application) { build(:clusters_applications_helm, version: '0.1.1') }
subject { described_class.new(application).as_json }
it 'has name' do
@@ -13,6 +13,10 @@ describe ClusterApplicationEntity do
expect(subject[:status]).to eq(:not_installable)
end
+ it 'has version' do
+ expect(subject[:version]).to eq('0.1.1')
+ end
+
it 'has no status_reason' do
expect(subject[:status_reason]).to be_nil
end
diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb
index 4beb50c70f8..ceb828a1cc5 100644
--- a/spec/serializers/container_tag_entity_spec.rb
+++ b/spec/serializers/container_tag_entity_spec.rb
@@ -16,7 +16,7 @@ describe ContainerTagEntity do
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: /image/, tags: %w[test])
+ stub_container_registry_tags(repository: /image/, tags: %w[test], with_manifest: true)
allow(request).to receive(:project).and_return(project)
allow(request).to receive(:current_user).and_return(user)
end
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index dbc40bddc30..d02b4c554b1 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -10,6 +10,7 @@ describe GroupChildEntity do
before do
allow(request).to receive(:current_user).and_return(user)
+ stub_commonmark_sourcepos_disabled
end
shared_examples 'group child json' do
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 561421d5ac8..376698a16df 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -31,23 +31,40 @@ describe MergeRequestWidgetEntity do
describe 'pipeline' do
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
- context 'when is up to date' do
- let(:req) { double('request', current_user: user, project: project) }
+ before do
+ allow_any_instance_of(MergeRequestPresenter).to receive(:can?).and_call_original
+ allow_any_instance_of(MergeRequestPresenter).to receive(:can?).with(user, :read_pipeline, anything).and_return(result)
+ end
- it 'returns pipeline' do
- pipeline_payload = PipelineDetailsEntity
- .represent(pipeline, request: req)
- .as_json
+ context 'when user has access to pipelines' do
+ let(:result) { true }
+
+ context 'when is up to date' do
+ let(:req) { double('request', current_user: user, project: project) }
+
+ it 'returns pipeline' do
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eq(pipeline_payload)
+ end
+ end
+
+ context 'when is not up to date' do
+ it 'returns nil' do
+ pipeline.update(sha: "not up to date")
- expect(subject[:pipeline]).to eq(pipeline_payload)
+ expect(subject[:pipeline]).to eq(nil)
+ end
end
end
- context 'when is not up to date' do
- it 'returns nil' do
- pipeline.update(sha: "not up to date")
+ context 'when user does not have access to pipelines' do
+ let(:result) { false }
- expect(subject[:pipeline]).to be_nil
+ it 'does not have pipeline' do
+ expect(subject[:pipeline]).to eq(nil)
end
end
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 5aa7165e135..d37ca13ebd2 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -69,14 +69,14 @@ describe Members::DestroyService do
it 'calls Member#after_decline_request' do
expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
- described_class.new(current_user).execute(member)
+ described_class.new(current_user).execute(member, opts)
end
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
- described_class.new(member_user).execute(member)
+ described_class.new(member_user).execute(member, opts)
end
end
end
@@ -159,7 +159,7 @@ describe Members::DestroyService do
end
it_behaves_like 'a service destroying a member' do
- let(:opts) { { skip_authorization: true } }
+ let(:opts) { { skip_authorization: true, skip_subresources: true } }
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
@@ -168,12 +168,14 @@ describe Members::DestroyService do
end
it_behaves_like 'a service destroying a member' do
- let(:opts) { { skip_authorization: true } }
+ let(:opts) { { skip_authorization: true, skip_subresources: true } }
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
context 'when current user can destroy the given access requester' do
+ let(:opts) { { skip_subresources: true } }
+
before do
group_project.add_maintainer(current_user)
group.add_owner(current_user)
@@ -229,4 +231,54 @@ describe Members::DestroyService do
end
end
end
+
+ context 'subresources' do
+ let(:user) { create(:user) }
+ let(:member_user) { create(:user) }
+ let(:opts) { {} }
+
+ let(:group) { create(:group, :public) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subsubgroup) { create(:group, parent: subgroup) }
+ let(:subsubproject) { create(:project, group: subsubgroup) }
+
+ let(:group_project) { create(:project, :public, group: group) }
+ let(:control_project) { create(:project, group: subsubgroup) }
+
+ before do
+ create(:group_member, :developer, group: subsubgroup, user: member_user)
+
+ subsubproject.add_developer(member_user)
+ control_project.add_maintainer(user)
+ group.add_owner(user)
+
+ group_member = create(:group_member, :developer, group: group, user: member_user)
+
+ described_class.new(user).execute(group_member, opts)
+ end
+
+ it 'removes the project membership' do
+ expect(group_project.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the group membership' do
+ expect(group.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the subgroup membership', :postgresql do
+ expect(subgroup.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the subsubgroup membership', :postgresql do
+ expect(subsubgroup.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the subsubproject membership', :postgresql do
+ expect(subsubproject.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'does not remove the user from the control project' do
+ expect(control_project.members.map(&:user)).to include(user)
+ end
+ end
end
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index ff85c261cd4..9aaccb4bffe 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -45,6 +45,15 @@ describe Notes::BuildService do
end
end
+ context 'when user has no access to discussion' do
+ it 'sets an error' do
+ another_user = create(:user)
+ new_note = described_class.new(project, another_user, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute
+
+ expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
+ end
+ end
+
context 'personal snippet note' do
def reply(note, user = nil)
user ||= create(:user)
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 80b015d4cd0..1b9ba42cfd6 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -127,6 +127,10 @@ describe Notes::CreateService do
create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo)
end
+ before do
+ project_with_repo.add_maintainer(user)
+ end
+
context 'when eligible to have a note diff file' do
let(:new_opts) do
opts.merge(in_reply_to_discussion_id: nil,
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index d20e712d365..6a5a6989607 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1646,6 +1646,23 @@ describe NotificationService, :mailer do
should_not_email(@u_guest_custom)
should_not_email(@u_disabled)
end
+
+ context 'users not having access to the new location' do
+ it 'does not send email' do
+ old_user = create(:user)
+ ProjectAuthorization.create!(project: project, user: old_user, access_level: Gitlab::Access::GUEST)
+
+ build_group(project)
+ reset_delivered_emails!
+
+ notification.project_was_moved(project, "gitlab/gitlab")
+
+ should_email(@g_watcher)
+ should_email(@g_global_watcher)
+ should_email(project.creator)
+ should_not_email(old_user)
+ end
+ end
end
context 'user with notifications disabled' do
@@ -2232,8 +2249,8 @@ describe NotificationService, :mailer do
# Users in the project's group but not part of project's team
# with different notification settings
- def build_group(project)
- group = create_nested_group
+ def build_group(project, visibility: :public)
+ group = create_nested_group(visibility)
project.update(namespace_id: group.id)
# Group member: global=disabled, group=watch
@@ -2249,10 +2266,10 @@ describe NotificationService, :mailer do
# Creates a nested group only if supported
# to avoid errors on MySQL
- def create_nested_group
+ def create_nested_group(visibility)
if Group.supports_nested_objects?
- parent_group = create(:group, :public)
- child_group = create(:group, :public, parent: parent_group)
+ parent_group = create(:group, visibility)
+ child_group = create(:group, visibility, parent: parent_group)
# Parent group member: global=disabled, parent_group=watch, child_group=global
@pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group)
@@ -2272,7 +2289,7 @@ describe NotificationService, :mailer do
child_group
else
- create(:group, :public)
+ create(:group, visibility)
end
end
diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb
index bc5366a3339..b8055a285f2 100644
--- a/spec/services/projects/after_rename_service_spec.rb
+++ b/spec/services/projects/after_rename_service_spec.rb
@@ -101,10 +101,10 @@ describe Projects::AfterRenameService do
end
context 'with hashed storage upgrade when renaming enabled' do
- it 'calls HashedStorageMigrationService with correct options' do
+ it 'calls HashedStorage::MigrationService with correct options' do
stub_application_setting(hashed_storage_enabled: true)
- expect_next_instance_of(::Projects::HashedStorageMigrationService) do |service|
+ expect_next_instance_of(::Projects::HashedStorage::MigrationService) do |service|
expect(service).to receive(:execute).and_return(true)
end
diff --git a/spec/services/projects/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
new file mode 100644
index 00000000000..0659130bed2
--- /dev/null
+++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::ContainerRepository::CleanupTagsService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :private) }
+ set(:repository) { create(:container_repository, :root, project: project) }
+
+ let(:service) { described_class.new(project, user, params) }
+
+ before do
+ project.add_maintainer(user)
+
+ stub_feature_flags(container_registry_cleanup: true)
+
+ stub_container_registry_config(enabled: true)
+
+ stub_container_registry_tags(
+ repository: repository.path,
+ tags: %w(latest A Ba Bb C D E))
+
+ stub_tag_digest('latest', 'sha256:configA')
+ stub_tag_digest('A', 'sha256:configA')
+ stub_tag_digest('Ba', 'sha256:configB')
+ stub_tag_digest('Bb', 'sha256:configB')
+ stub_tag_digest('C', 'sha256:configC')
+ stub_tag_digest('D', 'sha256:configD')
+ stub_tag_digest('E', nil)
+
+ stub_digest_config('sha256:configA', 1.hour.ago)
+ stub_digest_config('sha256:configB', 5.days.ago)
+ stub_digest_config('sha256:configC', 1.month.ago)
+ stub_digest_config('sha256:configD', nil)
+ end
+
+ describe '#execute' do
+ subject { service.execute(repository) }
+
+ context 'when no params are specified' do
+ let(:params) { {} }
+
+ it 'does not remove anything' do
+ expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag)
+
+ is_expected.to include(status: :success, deleted: [])
+ end
+ end
+
+ context 'when regex matching everything is specified' do
+ let(:params) do
+ { 'name_regex' => '.*' }
+ end
+
+ it 'does remove B* and C' do
+ # The :A cannot be removed as config is shared with :latest
+ # The :E cannot be removed as it does not have valid manifest
+
+ expect_delete('sha256:configB').twice
+ expect_delete('sha256:configC')
+ expect_delete('sha256:configD')
+
+ is_expected.to include(status: :success, deleted: %w(D Bb Ba C))
+ end
+ end
+
+ context 'when regex matching specific tags is used' do
+ let(:params) do
+ { 'name_regex' => 'C|D' }
+ end
+
+ it 'does remove C and D' do
+ expect_delete('sha256:configC')
+ expect_delete('sha256:configD')
+
+ is_expected.to include(status: :success, deleted: %w(D C))
+ end
+ end
+
+ context 'when removing a tagged image that is used by another tag' do
+ let(:params) do
+ { 'name_regex' => 'Ba' }
+ end
+
+ it 'does not remove the tag' do
+ # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/21405
+
+ is_expected.to include(status: :success, deleted: [])
+ end
+ end
+
+ context 'when removing keeping only 3' do
+ let(:params) do
+ { 'name_regex' => '.*',
+ 'keep_n' => 3 }
+ end
+
+ it 'does remove C as it is oldest' do
+ expect_delete('sha256:configC')
+
+ is_expected.to include(status: :success, deleted: %w(C))
+ end
+ end
+
+ context 'when removing older than 1 day' do
+ let(:params) do
+ { 'name_regex' => '.*',
+ 'older_than' => '1 day' }
+ end
+
+ it 'does remove B* and C as they are older than 1 day' do
+ expect_delete('sha256:configB').twice
+ expect_delete('sha256:configC')
+
+ is_expected.to include(status: :success, deleted: %w(Bb Ba C))
+ end
+ end
+
+ context 'when combining all parameters' do
+ let(:params) do
+ { 'name_regex' => '.*',
+ 'keep_n' => 1,
+ 'older_than' => '1 day' }
+ end
+
+ it 'does remove B* and C' do
+ expect_delete('sha256:configB').twice
+ expect_delete('sha256:configC')
+
+ is_expected.to include(status: :success, deleted: %w(Bb Ba C))
+ end
+ end
+ end
+
+ private
+
+ def stub_tag_digest(tag, digest)
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_tag_digest)
+ .with(repository.path, tag) { digest }
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_manifest)
+ .with(repository.path, tag) do
+ { 'config' => { 'digest' => digest } } if digest
+ end
+ end
+
+ def stub_digest_config(digest, created_at)
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:blob)
+ .with(repository.path, digest, nil) do
+ { 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
+ end
+ end
+
+ def expect_delete(digest)
+ expect_any_instance_of(ContainerRegistry::Client)
+ .to receive(:delete_repository_tag)
+ .with(repository.path, digest)
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
index 28d8a95fe07..61dbb57ec08 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::HashedStorage::MigrateAttachmentsService do
subject(:service) { described_class.new(project, project.full_path, logger: nil) }
- let(:project) { create(:project, :legacy_storage) }
+ let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
@@ -28,6 +28,16 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
expect(File.file?(old_disk_path)).to be_falsey
expect(File.file?(new_disk_path)).to be_truthy
end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+
+ it 'sets skipped to false' do
+ service.execute
+
+ expect(service.skipped?).to be_falsey
+ end
end
context 'when original folder does not exist anymore' do
@@ -43,6 +53,16 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
expect(File.exist?(base_path(hashed_storage))).to be_falsey
expect(File.file?(new_disk_path)).to be_falsey
end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+
+ it 'sets skipped to true' do
+ service.execute
+
+ expect(service.skipped?).to be_truthy
+ end
end
context 'when target folder already exists' do
@@ -58,6 +78,18 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
end
end
+ context '#old_disk_path' do
+ it 'returns old disk_path for project' do
+ expect(service.old_disk_path).to eq(project.full_path)
+ end
+ end
+
+ context '#new_disk_path' do
+ it 'returns new disk_path for project' do
+ expect(service.new_disk_path).to eq(project.disk_path)
+ end
+ end
+
def base_path(storage)
File.join(FileUploader.root, storage.disk_path)
end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index b720f37ffdb..0772dc4b85b 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -8,9 +8,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
- subject(:service) { described_class.new(project, project.full_path) }
+ subject(:service) { described_class.new(project, project.disk_path) }
describe '#execute' do
+ let(:old_disk_path) { legacy_storage.disk_path }
+ let(:new_disk_path) { hashed_storage.disk_path }
+
before do
allow(service).to receive(:gitlab_shell) { gitlab_shell }
end
@@ -33,8 +36,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'renames project and wiki repositories' do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be hashed and not read-only' do
@@ -45,8 +48,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
end
it 'move operation is called for both repositories' do
- expect_move_repository(project.disk_path, hashed_storage.disk_path)
- expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+ expect_move_repository(old_disk_path, new_disk_path)
+ expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
service.execute
end
@@ -62,32 +65,27 @@ describe Projects::HashedStorage::MigrateRepositoryService do
context 'when one move fails' do
it 'rollsback repositories to original name' do
- from_name = project.disk_path
- to_name = hashed_storage.disk_path
allow(service).to receive(:move_repository).and_call_original
- allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only
+ allow(service).to receive(:move_repository).with(old_disk_path, new_disk_path).once { false } # will disable first move only
expect(service).to receive(:rollback_folder_move).and_call_original
service.execute
- expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
context 'when rollback fails' do
- let(:from_name) { legacy_storage.disk_path }
- let(:to_name) { hashed_storage.disk_path }
-
before do
hashed_storage.ensure_storage_path_exists
- gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, old_disk_path, new_disk_path)
end
- it 'does not try to move nil repository over hashed' do
- expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, from_name, to_name)
- expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
+ it 'does not try to move nil repository over existing' do
+ expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, old_disk_path, new_disk_path)
+ expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
service.execute
end
diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage/migration_service_spec.rb
index 5368c3828dd..b4647586363 100644
--- a/spec/services/projects/hashed_storage_migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migration_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::HashedStorageMigrationService do
+describe Projects::HashedStorage::MigrationService do
let(:project) { create(:project, :empty_repo, :wiki_repo, :legacy_storage) }
let(:logger) { double }
diff --git a/spec/services/projects/import_error_filter_spec.rb b/spec/services/projects/import_error_filter_spec.rb
new file mode 100644
index 00000000000..312b658de89
--- /dev/null
+++ b/spec/services/projects/import_error_filter_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::ImportErrorFilter do
+ it 'filters any full paths' do
+ message = 'Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file'
+
+ expect(described_class.filter_message(message)).to eq('Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]')
+ end
+
+ it 'filters any relative paths ignoring single slash ones' do
+ message = 'Error importing into my/project Permission denied @ unlink_internal - ../file/ and folder/../file'
+
+ expect(described_class.filter_message(message)).to eq('Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED] and [FILTERED]')
+ end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 06f865dc848..7faf0fc2868 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -136,12 +136,12 @@ describe Projects::ImportService do
end
it 'fails if repository import fails' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error.new('Failed to import the repository /a/b/c'))
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository"
+ expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]"
end
context 'when repository import scheduled' do
@@ -152,8 +152,11 @@ describe Projects::ImportService do
it 'downloads lfs objects if lfs_enabled is enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(true)
+
+ service = double
expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute).twice
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
+ expect(service).to receive(:execute).twice
subject.execute
end
@@ -211,8 +214,10 @@ describe Projects::ImportService do
it 'does not have a custom repository importer downloads lfs objects' do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
+ service = double
expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
+ expect(service).to receive(:execute).twice
subject.execute
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index d7a2829d5f8..f222c52199f 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -37,8 +37,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
describe '#execute' do
it 'retrieves each download link of every non existent lfs object' do
- subject.execute(new_oids).each do |oid, link|
- expect(link).to eq "#{import_url}/gitlab-lfs/objects/#{oid}"
+ subject.execute(new_oids).each do |lfs_download_object|
+ expect(lfs_download_object.link).to eq "#{import_url}/gitlab-lfs/objects/#{lfs_download_object.oid}"
end
end
@@ -50,8 +50,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
it 'adds credentials to the download_link' do
result = subject.execute(new_oids)
- result.each do |oid, link|
- expect(link.starts_with?('http://user:password@')).to be_truthy
+ result.each do |lfs_download_object|
+ expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_truthy
end
end
end
@@ -60,8 +60,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
it 'does not add any credentials' do
result = subject.execute(new_oids)
- result.each do |oid, link|
- expect(link.starts_with?('http://user:password@')).to be_falsey
+ result.each do |lfs_download_object|
+ expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey
end
end
end
@@ -74,8 +74,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
it 'downloads without any credentials' do
result = subject.execute(new_oids)
- result.each do |oid, link|
- expect(link.starts_with?('http://user:password@')).to be_falsey
+ result.each do |lfs_download_object|
+ expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey
end
end
end
@@ -92,7 +92,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
describe '#parse_response_links' do
it 'does not add oid entry if href not found' do
- expect(Rails.logger).to receive(:error).with("Link for Lfs Object with oid whatever not found or invalid.")
+ expect(subject).to receive(:log_error).with("Link for Lfs Object with oid whatever not found or invalid.")
result = subject.send(:parse_response_links, invalid_object_response)
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index fcc87196d5a..876beb39801 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -2,68 +2,156 @@ require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
let(:project) { create(:project) }
- let(:oid) { '9e548e25631dd9ce6b43afd6359ab76da2819d6a5b474e66118c7819e1d8b3e8' }
- let(:download_link) { "http://gitlab.com/#{oid}" }
let(:lfs_content) { SecureRandom.random_bytes(10) }
+ let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
+ let(:download_link) { "http://gitlab.com/#{oid}" }
+ let(:size) { lfs_content.size }
+ let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link) }
+ let(:local_request_setting) { false }
- subject { described_class.new(project) }
+ subject { described_class.new(project, lfs_object) }
before do
+ ApplicationSetting.create_from_defaults
+
+ stub_application_setting(allow_local_requests_from_hooks_and_services: local_request_setting)
allow(project).to receive(:lfs_enabled?).and_return(true)
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ end
+
+ shared_examples 'lfs temporal file is removed' do
+ it do
+ subject.execute
- allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(false)
+ expect(File.exist?(subject.send(:tmp_filename))).to be false
+ end
+ end
+
+ shared_examples 'no lfs object is created' do
+ it do
+ expect { subject.execute }.not_to change { LfsObject.count }
+ end
+
+ it 'returns error result' do
+ expect(subject.execute[:status]).to eq :error
+ end
+
+ it 'an error is logged' do
+ expect(subject).to receive(:log_error)
+
+ subject.execute
+ end
+
+ it_behaves_like 'lfs temporal file is removed'
+ end
+
+ shared_examples 'lfs object is created' do
+ it do
+ expect(subject).to receive(:download_and_save_file!).and_call_original
+
+ expect { subject.execute }.to change { LfsObject.count }.by(1)
+ end
+
+ it 'returns success result' do
+ expect(subject.execute[:status]).to eq :success
+ end
+
+ it_behaves_like 'lfs temporal file is removed'
end
describe '#execute' do
context 'when file download succeeds' do
- it 'a new lfs object is created' do
- expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1)
+ before do
+ WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
end
+ it_behaves_like 'lfs object is created'
+
it 'has the same oid' do
- subject.execute(oid, download_link)
+ subject.execute
expect(LfsObject.first.oid).to eq oid
end
+ it 'has the same size' do
+ subject.execute
+
+ expect(LfsObject.first.size).to eq size
+ end
+
it 'stores the content' do
- subject.execute(oid, download_link)
+ subject.execute
expect(File.binread(LfsObject.first.file.file.file)).to eq lfs_content
end
end
context 'when file download fails' do
- it 'no lfs object is created' do
- expect { subject.execute(oid, download_link) }.to change { LfsObject.count }
+ before do
+ allow(Gitlab::HTTP).to receive(:get).and_return(code: 500, 'success?' => false)
+ end
+
+ it_behaves_like 'no lfs object is created'
+
+ it 'raise StandardError exception' do
+ expect(subject).to receive(:download_and_save_file!).and_raise(StandardError)
+
+ subject.execute
+ end
+ end
+
+ context 'when downloaded lfs file has a different size' do
+ let(:size) { 1 }
+
+ before do
+ WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ end
+
+ it_behaves_like 'no lfs object is created'
+
+ it 'raise SizeError exception' do
+ expect(subject).to receive(:download_and_save_file!).and_raise(described_class::SizeError)
+
+ subject.execute
+ end
+ end
+
+ context 'when downloaded lfs file has a different oid' do
+ before do
+ WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
+ end
+
+ it_behaves_like 'no lfs object is created'
+
+ it 'raise OidError exception' do
+ expect(subject).to receive(:download_and_save_file!).and_raise(described_class::OidError)
+
+ subject.execute
end
end
context 'when credentials present' do
let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" }
+ let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
before do
WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
end
it 'the request adds authorization headers' do
- subject.execute(oid, download_link_with_credentials)
+ subject
end
end
context 'when localhost requests are allowed' do
let(:download_link) { 'http://192.168.2.120' }
+ let(:local_request_setting) { true }
before do
- allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(true)
+ WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
end
- it 'downloads the file' do
- expect(subject).to receive(:download_and_save_file).and_call_original
-
- expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.by(1)
- end
+ it_behaves_like 'lfs object is created'
end
context 'when a bad URL is used' do
@@ -71,7 +159,9 @@ describe Projects::LfsPointers::LfsDownloadService do
with_them do
it 'does not download the file' do
- expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count }
+ expect(subject).not_to receive(:download_lfs_file!)
+
+ expect { subject.execute }.not_to change { LfsObject.count }
end
end
end
@@ -85,15 +175,11 @@ describe Projects::LfsPointers::LfsDownloadService do
WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
end
- it 'does not follow the redirection' do
- expect(Rails.logger).to receive(:error).with(/LFS file with oid #{oid} couldn't be downloaded/)
-
- expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count }
- end
+ it_behaves_like 'no lfs object is created'
end
end
- context 'that is valid' do
+ context 'that is not blocked' do
let(:redirect_link) { "http://example.com/"}
before do
@@ -101,21 +187,35 @@ describe Projects::LfsPointers::LfsDownloadService do
WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
end
- it 'follows the redirection' do
- expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1)
- end
+ it_behaves_like 'lfs object is created'
+ end
+ end
+
+ context 'when the lfs object attributes are invalid' do
+ let(:oid) { 'foobar' }
+
+ before do
+ expect(lfs_object).to be_invalid
+ end
+
+ it_behaves_like 'no lfs object is created'
+
+ it 'does not download the file' do
+ expect(subject).not_to receive(:download_lfs_file!)
+
+ subject.execute
end
end
context 'when an lfs object with the same oid already exists' do
before do
- create(:lfs_object, oid: 'oid')
+ create(:lfs_object, oid: oid)
end
it 'does not download the file' do
- expect(subject).not_to receive(:download_and_save_file)
+ expect(subject).not_to receive(:download_lfs_file!)
- subject.execute('oid', download_link)
+ subject.execute
end
end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 36b619ba9be..8b70845befe 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -5,24 +5,27 @@ describe Projects::UpdatePagesService do
set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') }
let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') }
- let(:extension) { 'zip' }
- let(:file) { fixture_file_upload("spec/fixtures/pages.#{extension}") }
- let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.#{extension}") }
- let(:metadata) do
- filename = "spec/fixtures/pages.#{extension}.meta"
- fixture_file_upload(filename) if File.exist?(filename)
- end
+ let(:file) { fixture_file_upload("spec/fixtures/pages.zip") }
+ let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.zip") }
+ let(:metadata_filename) { "spec/fixtures/pages.zip.meta" }
+ let(:metadata) { fixture_file_upload(metadata_filename) if File.exist?(metadata_filename) }
subject { described_class.new(project, build) }
before do
+ stub_feature_flags(safezip_use_rubyzip: true)
+
project.remove_pages
end
- context 'legacy artifacts' do
- let(:extension) { 'zip' }
+ context '::TMP_EXTRACT_PATH' do
+ subject { described_class::TMP_EXTRACT_PATH }
+ it { is_expected.not_to match(Gitlab::PathRegex.namespace_format_regex) }
+ end
+
+ context 'legacy artifacts' do
before do
build.update(legacy_artifacts_file: file)
build.update(legacy_artifacts_metadata: metadata)
@@ -132,6 +135,20 @@ describe Projects::UpdatePagesService do
end
end
+ context 'when using pages with non-writeable public' do
+ let(:file) { fixture_file_upload("spec/fixtures/pages_non_writeable.zip") }
+
+ context 'when using RubyZip' do
+ before do
+ stub_feature_flags(safezip_use_rubyzip: true)
+ end
+
+ it 'succeeds to extract' do
+ expect(execute).to eq(:success)
+ end
+ end
+ end
+
context 'when timeout happens by DNS error' do
before do
allow_any_instance_of(described_class)
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index 14c43b46c15..72467091791 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -44,7 +44,7 @@ describe ResourceEvents::MergeIntoNotesService do
create_event(created_at: time, user: user2)
create_event(created_at: 1.day.ago, label: label2)
- notes = described_class.new(resource, user).execute()
+ notes = described_class.new(resource, user).execute
expected = [
"added #{label.to_reference} label and removed #{label2.to_reference} label",
@@ -61,7 +61,7 @@ describe ResourceEvents::MergeIntoNotesService do
event = create_event(created_at: 1.day.ago)
notes = described_class.new(resource, user,
- last_fetched_at: 2.days.ago.to_i).execute()
+ last_fetched_at: 2.days.ago.to_i).execute
expect(notes.count).to eq 1
expect(notes.first.discussion_id).to eq event.discussion_id
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index e5ca1c155ed..8e77d582eb4 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -134,12 +134,11 @@ describe Suggestions::ApplyService do
end
end
- context 'when diff ref from position is different from repo diff refs' do
+ context 'when HEAD from position is different from source branch HEAD on repo' do
it 'returns error message' do
- outdated_refs = Gitlab::Diff::DiffRefs.new(base_sha: 'foo', start_sha: 'bar', head_sha: 'outdated')
-
allow(suggestion).to receive(:appliable?) { true }
- allow(suggestion.position).to receive(:diff_refs) { outdated_refs }
+ allow(suggestion.position).to receive(:head_sha) { 'old-sha' }
+ allow(suggestion.noteable).to receive(:source_branch_sha) { 'new-sha' }
result = subject.execute(suggestion)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 72684caad32..97e7a019222 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -127,11 +127,6 @@ RSpec.configure do |config|
.and_return(false)
end
- config.before(:suite) do
- # Set latest release blog post URL for "What's new?" link
- Gitlab::ReleaseBlogPost.instance.instance_variable_set(:@url, 'https://about.gitlab.com')
- end
-
config.before(:example, :quarantine) do
# Skip tests in quarantine unless we explicitly focus on them.
skip('In quarantine') unless config.inclusion_filter[:quarantine]
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 6930b809048..9dc89b483b2 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -369,6 +369,6 @@ module KubernetesHelpers
end
def empty_deployment_rollout_status
- ::Gitlab::Kubernetes::RolloutStatus.from_deployments()
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments
end
end
diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
index acd9cce6a67..7d8d7750bf3 100644
--- a/spec/support/helpers/rake_helpers.rb
+++ b/spec/support/helpers/rake_helpers.rb
@@ -14,7 +14,7 @@ module RakeHelpers
end
def silence_progress_bar
- allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double().as_null_object)
+ allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double.as_null_object)
end
def main_object
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index 90618ba5b19..f4f0415985c 100644
--- a/spec/support/helpers/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
@@ -1,3 +1,5 @@
+require_relative 'wait_for_requests'
+
# Select2 ajax programmatic helper
# It allows you to select value from select2
#
@@ -11,9 +13,13 @@
#
module Select2Helper
+ include WaitForRequests
+
def select2(value, options = {})
raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
+ wait_for_requests unless options[:async]
+
selector = options.fetch(:from)
first(selector, visible: false)
diff --git a/spec/support/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb
index 36b90fc68d6..1c2f474a015 100644
--- a/spec/support/helpers/stub_env.rb
+++ b/spec/support/helpers/stub_env.rb
@@ -18,7 +18,7 @@ module StubENV
allow(ENV).to receive(:[]).with(key).and_return(value)
allow(ENV).to receive(:key?).with(key).and_return(true)
allow(ENV).to receive(:fetch).with(key).and_return(value)
- allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val|
+ allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val|
value || default_val
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 2933f2c78dc..4cb3b18df85 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -36,31 +36,47 @@ module StubGitlabCalls
.to receive(:full_access_token).and_return('token')
end
- def stub_container_registry_tags(repository: :any, tags:)
+ def stub_container_registry_tags(repository: :any, tags: [], with_manifest: false)
repository = any_args if repository == :any
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:repository_tags).with(repository)
.and_return({ 'tags' => tags })
- allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:repository_manifest).with(repository, anything)
- .and_return(stub_container_registry_tag_manifest)
+ if with_manifest
+ tags.each do |tag|
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_tag_digest)
+ .with(repository, tag)
+ .and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3' \
+ '72b088dac5b6d7ad7d49cd620d85cf72a15')
+ end
- allow_any_instance_of(ContainerRegistry::Client)
- .to receive(:blob).with(repository, anything, 'application/octet-stream')
- .and_return(stub_container_registry_blob)
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:repository_manifest).with(repository, anything)
+ .and_return(stub_container_registry_tag_manifest_content)
+
+ allow_any_instance_of(ContainerRegistry::Client)
+ .to receive(:blob).with(repository, anything, 'application/octet-stream')
+ .and_return(stub_container_registry_blob_content)
+ end
+ end
+
+ def stub_commonmark_sourcepos_disabled
+ allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
+ .to receive(:render_options)
+ .and_return(Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS)
end
private
- def stub_container_registry_tag_manifest
+ def stub_container_registry_tag_manifest_content
fixture_path = 'spec/fixtures/container_registry/tag_manifest.json'
JSON.parse(File.read(Rails.root + fixture_path))
end
- def stub_container_registry_blob
+ def stub_container_registry_blob_content
fixture_path = 'spec/fixtures/container_registry/config_blob.json'
File.read(Rails.root + fixture_path)
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
index d86838719d4..98ab04c5636 100644
--- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -2,18 +2,12 @@ shared_examples 'set sort order from user preference' do
describe '#set_sort_order_from_user_preference' do
# There is no issuable_sorting_field defined in any CE controllers yet,
# however any other field present in user_preferences table can be used for testing.
- let(:sorting_field) { :issue_notes_filter }
- let(:sorting_param) { 'any' }
-
- before do
- allow(controller).to receive(:issuable_sorting_field).and_return(sorting_field)
- end
context 'when database is in read-only mode' do
it 'it does not update user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
- expect_any_instance_of(UserPreference).not_to receive(:update_attribute).with(sorting_field, sorting_param)
+ expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
@@ -23,7 +17,7 @@ shared_examples 'set sort order from user preference' do
it 'updates user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(false)
- expect_any_instance_of(UserPreference).to receive(:update_attribute).with(sorting_field, sorting_param)
+ expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index be902d7c679..6b50670c3c0 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -58,7 +58,7 @@ describe 'rake gitlab:storage:*' do
context '0 legacy projects' do
it 'does nothing' do
- expect(StorageMigratorWorker).not_to receive(:perform_async)
+ expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async)
run_rake_task(task)
end
@@ -72,9 +72,9 @@ describe 'rake gitlab:storage:*' do
stub_env('BATCH' => 1)
end
- it 'enqueues one StorageMigratorWorker per project' do
+ it 'enqueues one HashedStorage::MigratorWorker per project' do
projects.each do |project|
- expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id)
+ expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(project.id, project.id)
end
run_rake_task(task)
@@ -86,10 +86,10 @@ describe 'rake gitlab:storage:*' do
stub_env('BATCH' => 2)
end
- it 'enqueues one StorageMigratorWorker per 2 projects' do
+ it 'enqueues one HashedStorage::MigratorWorker per 2 projects' do
projects.map(&:id).sort.each_slice(2) do |first, last|
last ||= first
- expect(StorageMigratorWorker).to receive(:perform_async).with(first, last)
+ expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(first, last)
end
run_rake_task(task)
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 2852aa380b2..d9f05e5f94f 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -57,4 +57,58 @@ describe 'layouts/nav/sidebar/_project' do
expect(rendered).to have_link('Releases', href: project_releases_path(project))
end
end
+
+ describe 'wiki entry tab' do
+ let(:can_read_wiki) { true }
+
+ before do
+ allow(view).to receive(:can?).with(nil, :read_wiki, project).and_return(can_read_wiki)
+ end
+
+ describe 'when wiki is enabled' do
+ it 'shows the wiki tab with the wiki internal link' do
+ render
+
+ expect(rendered).to have_link('Wiki', href: project_wiki_path(project, :home))
+ end
+ end
+
+ describe 'when wiki is disabled' do
+ let(:can_read_wiki) { false }
+
+ it 'does not show the wiki tab' do
+ render
+
+ expect(rendered).not_to have_link('Wiki', href: project_wiki_path(project, :home))
+ end
+ end
+ end
+
+ describe 'external wiki entry tab' do
+ let(:properties) { { 'external_wiki_url' => 'https://gitlab.com' } }
+ let(:service_status) { true }
+
+ before do
+ project.create_external_wiki_service(active: service_status, properties: properties)
+ project.reload
+ end
+
+ context 'when it is active' do
+ it 'shows the external wiki tab with the external wiki service link' do
+ render
+
+ expect(rendered).to have_link('External Wiki', href: properties['external_wiki_url'])
+ end
+ end
+
+ context 'when it is disabled' do
+ let(:service_status) { false }
+
+ it 'does not show the external wiki tab' do
+ render
+
+ expect(rendered).not_to have_link('External Wiki', href: project_wiki_path(project, :home))
+ end
+ end
+ end
end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 006c93686d5..908ecb898e4 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -23,7 +23,7 @@ describe 'projects/_home_panel' do
it 'makes it possible to set notification level' do
render
- expect(view).to render_template('projects/buttons/_notifications')
+ expect(view).to render_template('shared/notifications/_new_button')
expect(rendered).to have_selector('.notification-dropdown')
end
end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 2fdd28a3be4..1086546c10d 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -9,6 +9,7 @@ describe 'projects/commit/_commit_box.html.haml' do
assign(:commit, project.commit)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:can_collaborate_with_project?).and_return(false)
+ project.add_developer(user)
end
it 'shows the commit SHA' do
@@ -48,7 +49,6 @@ describe 'projects/commit/_commit_box.html.haml' do
context 'viewing a commit' do
context 'as a developer' do
before do
- project.add_developer(user)
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
end
@@ -60,6 +60,10 @@ describe 'projects/commit/_commit_box.html.haml' do
end
context 'as a non-developer' do
+ before do
+ project.add_guest(user)
+ end
+
it 'does not have a link to create a new tag' do
render
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 8c845251765..5cff7694029 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'projects/issues/_related_branches' do
include Devise::Test::ControllerHelpers
+ let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:branch) { project.repository.find_branch('feature') }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') }
@@ -11,6 +12,9 @@ describe 'projects/issues/_related_branches' do
assign(:project, project)
assign(:related_branches, ['feature'])
+ project.add_developer(user)
+ allow(view).to receive(:current_user).and_return(user)
+
render
end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
new file mode 100644
index 00000000000..ff88efd0e31
--- /dev/null
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/issues/show' do
+ let(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ assign(:project, project)
+ assign(:issue, issue)
+ assign(:noteable, issue)
+ stub_template 'shared/issuable/_sidebar' => ''
+ stub_template 'projects/issues/_discussion' => ''
+ allow(view).to receive(:issuable_meta).and_return('')
+ end
+
+ context 'when the issue is closed' do
+ before do
+ allow(issue).to receive(:closed?).and_return(true)
+ end
+
+ it 'shows "Closed (moved)" if an issue has been moved' do
+ allow(issue).to receive(:moved?).and_return(true)
+
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ end
+
+ it 'shows "Closed" if an issue has not been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed')
+ end
+ end
+
+ context 'when the issue is open' do
+ before do
+ allow(issue).to receive(:closed?).and_return(false)
+ allow(issue).to receive(:disscussion_locked).and_return(false)
+ end
+
+ it 'shows "Open" if an issue has been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open')
+ end
+ end
+end
diff --git a/spec/workers/cleanup_container_repository_worker_spec.rb b/spec/workers/cleanup_container_repository_worker_spec.rb
new file mode 100644
index 00000000000..5bee7294010
--- /dev/null
+++ b/spec/workers/cleanup_container_repository_worker_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_state do
+ let(:repository) { create(:container_repository) }
+ let(:project) { repository.project }
+ let(:user) { project.owner }
+ let(:params) { { key: 'value' } }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ let(:service) { instance_double(Projects::ContainerRepository::CleanupTagsService) }
+
+ before do
+ allow(Projects::ContainerRepository::CleanupTagsService).to receive(:new)
+ .with(project, user, params).and_return(service)
+ end
+
+ it 'executes the destroy service' do
+ expect(service).to receive(:execute)
+
+ subject.perform(user.id, repository.id, params)
+ end
+
+ it 'does not raise error when user could not be found' do
+ expect do
+ subject.perform(-1, repository.id, params)
+ end.not_to raise_error
+ end
+
+ it 'does not raise error when repository could not be found' do
+ expect do
+ subject.perform(user.id, -1, params)
+ end.not_to raise_error
+ end
+
+ context 'when executed twice in short period' do
+ it 'executes service only for the first time' do
+ expect(service).to receive(:execute).once
+
+ 2.times { subject.perform(user.id, repository.id, params) }
+ end
+ end
+ end
+end
diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index 808084c8f7c..a85f820a3eb 100644
--- a/spec/workers/storage_migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-describe StorageMigratorWorker do
+describe HashedStorage::MigratorWorker do
subject(:worker) { described_class.new }
let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
let(:ids) { projects.map(&:id) }
describe '#perform' do
it 'delegates to MigratorService' do
- expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10)
+ expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(start: 5, finish: 10)
worker.perform(5, 10)
end
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
index 3703320418b..333eb6a0569 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -4,12 +4,13 @@ describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
include ExclusiveLeaseHelpers
describe '#perform' do
- let(:project) { create(:project, :empty_repo) }
+ let(:project) { create(:project, :empty_repo, :legacy_storage) }
let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" }
- let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT }
+ let(:lease_timeout) { described_class::LEASE_TIMEOUT }
+ let(:migration_service) { ::Projects::HashedStorage::MigrationService }
it 'skips when project no longer exists' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(migration_service).not_to receive(:new)
subject.perform(-1)
end
@@ -17,29 +18,29 @@ describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
it 'skips when project is pending delete' do
pending_delete_project = create(:project, :empty_repo, pending_delete: true)
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(migration_service).not_to receive(:new)
subject.perform(pending_delete_project.id)
end
- it 'delegates removal to service class when have exclusive lease' do
+ it 'delegates migration to service class when we have exclusive lease' do
stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout)
- migration_service = spy
+ service_spy = spy
- allow(::Projects::HashedStorageMigrationService)
+ allow(migration_service)
.to receive(:new).with(project, project.full_path, logger: subject.logger)
- .and_return(migration_service)
+ .and_return(service_spy)
subject.perform(project.id)
- expect(migration_service).to have_received(:execute)
+ expect(service_spy).to have_received(:execute)
end
- it 'skips when dont have lease when dont have exclusive lease' do
+ it 'skips when it cant acquire the exclusive lease' do
stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(migration_service).not_to receive(:new)
subject.perform(project.id)
end
diff --git a/yarn.lock b/yarn.lock
index bb948ad703c..5c9139fdbfa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -653,15 +653,15 @@
eslint-plugin-promise "^4.0.1"
eslint-plugin-vue "^5.0.0"
-"@gitlab/svgs@^1.47.0":
- version "1.47.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.47.0.tgz#c03dda41aefd3889cbfed95a391836106ae2ac4d"
- integrity sha512-0Bx/HxqR8xpqqaLnZiFAHIh1jTAFQPFToVZ6Wi3QyhsAwmXRAbgw1SlkRMZ7w3e6l+G71Wnw+GnI4rx1gK8JLQ==
+"@gitlab/svgs@^1.48.0":
+ version "1.48.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.48.0.tgz#7b2e20e357d85aa46e905e6ca51b0b4184ae2794"
+ integrity sha512-9lRsfqN0W3JxopiXnTzvDY31O465jMTGNKpiOCXy7uAMfwZA6UsRsc7Pp369uKnOLR0duXUGOxOv4NGsK6AeXw==
-"@gitlab/ui@^1.20.0":
- version "1.20.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.20.0.tgz#50bd4b092646a2c6337f0f462779af8e702dda05"
- integrity sha512-EJgrqon/tYCUPoOgnNNAXbrDXOEAajJwKHr4aR2R6vkJI3kVZiq66RNIe5ftGIUoNqYCDnRIkpLyo7MqzJPgcw==
+"@gitlab/ui@^1.22.1":
+ version "1.22.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.22.1.tgz#92ed77216c5702776049b9ac41eb717c1acd864e"
+ integrity sha512-pWbEaXOOcp8Xt2TjJtPas3lXwWVvizrBOf0M8yN0XAn2GgIRCVnRMpjNEN7/oNeBcEM9CrmPYApEM/hZO+maqQ==
dependencies:
babel-standalone "^6.26.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -679,6 +679,11 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
+"@types/anymatch@*":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff"
+ integrity sha512-7WcbyctkE8GTzogDb0ulRAEw7v8oIS54ft9mQTU7PfM0hp5e+8kpa+HeQ7IQrFbKtJXBKcZ4bh+Em9dTw5L6AQ==
+
"@types/async@2.0.50":
version "2.0.50"
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb"
@@ -733,6 +738,29 @@
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==
+"@types/tapable@*":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
+ integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
+
+"@types/uglify-js@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
+ integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==
+ dependencies:
+ source-map "^0.6.1"
+
+"@types/webpack@^4.4.19":
+ version "4.4.23"
+ resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.23.tgz#059d6f4598cfd65ddee0e2db38317ef989696712"
+ integrity sha512-WswyG+2mRg0ul/ytPpCSWo+kOlVVPW/fKCBEVwqmPVC/2ffWEwhsCEQgnFbWDf8EWId2qGcpL623EjLfNTRk9A==
+ dependencies:
+ "@types/anymatch" "*"
+ "@types/node" "*"
+ "@types/tapable" "*"
+ "@types/uglify-js" "*"
+ source-map "^0.6.0"
+
"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
@@ -936,12 +964,10 @@ accepts@~1.3.4, accepts@~1.3.5:
mime-types "~2.1.18"
negotiator "0.6.1"
-acorn-dynamic-import@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278"
- integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==
- dependencies:
- acorn "^5.0.0"
+acorn-dynamic-import@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+ integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
acorn-globals@^4.1.0:
version "4.3.0"
@@ -961,15 +987,15 @@ acorn-walk@^6.0.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
-acorn@^5.0.0, acorn@^5.5.3, acorn@^5.6.2, acorn@^5.7.3:
+acorn@^5.5.3, acorn@^5.7.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-acorn@^6.0.1, acorn@^6.0.2:
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754"
- integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==
+acorn@^6.0.1, acorn@^6.0.2, acorn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.5.tgz#81730c0815f3f3b34d8efa95cb7430965f4d887a"
+ integrity sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==
after@0.8.2:
version "0.8.2"
@@ -2106,7 +2132,7 @@ check-types@^7.3.0:
resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
integrity sha1-Ro9XGkQ1wkJI9f0MsOjYfDw0Hn0=
-chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3:
+chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==
@@ -2282,12 +2308,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
-colors@^1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
- integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
-
-colors@^1.1.2:
+colors@^1.1.0, colors@^1.1.2:
version "1.3.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==
@@ -3320,7 +3341,7 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
-duplexer@^0.1.1, duplexer@~0.1.1:
+duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=
@@ -3820,19 +3841,6 @@ eve-raphael@0.5.0:
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA=
-event-stream@~3.3.0:
- version "3.3.4"
- resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
- integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=
- dependencies:
- duplexer "~0.1.1"
- from "~0"
- map-stream "~0.1.0"
- pause-stream "0.0.11"
- split "0.3"
- stream-combiner "~0.0.4"
- through "~2.3.1"
-
eventemitter3@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
@@ -4094,6 +4102,13 @@ fastparse@^1.1.1:
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
integrity sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=
+fault@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
+ integrity sha512-o2eo/X2syzzERAtN5LcGbiVQ0WwZSlN3qLtadwAz3X8Bu+XWD16dja/KMsjZLiQr+BLGPDnHGkc4yUJf1Xpkpw==
+ dependencies:
+ format "^0.2.2"
+
faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
@@ -4313,6 +4328,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
+format@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+ integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+
formdata-polyfill@^3.0.11:
version "3.0.11"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-3.0.11.tgz#c82b4b4bea3356c0a6752219e54ce1edb2a7fb5b"
@@ -4343,11 +4363,6 @@ from2@^2.1.0, from2@^2.1.1:
inherits "^2.0.1"
readable-stream "^2.0.0"
-from@~0:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
- integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
-
fs-access@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a"
@@ -4816,7 +4831,7 @@ he@^1.1.0, he@^1.1.1:
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
-highlight.js@^9.13.1:
+highlight.js@^9.13.1, highlight.js@~9.13.0:
version "9.13.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
@@ -6467,6 +6482,13 @@ lightercollective@^0.1.0:
resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.1.0.tgz#70df102c530dcb8d0ccabfe6175a8d00d5f61300"
integrity sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ==
+linkify-it@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db"
+ integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==
+ dependencies:
+ uc.micro "^1.0.1"
+
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -6628,6 +6650,14 @@ lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=
+lowlight@^1.11.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
+ integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
+ dependencies:
+ fault "^1.0.2"
+ highlight.js "~9.13.0"
+
lru-cache@2.2.x:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
@@ -6679,11 +6709,6 @@ map-cache@^0.2.2:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
-map-stream@~0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
- integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=
-
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -6691,6 +6716,17 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+markdown-it@^8.4.2:
+ version "8.4.2"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
+ integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==
+ dependencies:
+ argparse "^1.0.7"
+ entities "~1.1.1"
+ linkify-it "^2.0.0"
+ mdurl "^1.0.1"
+ uc.micro "^1.0.5"
+
marked@^0.3.12, marked@~0.3.6:
version "0.3.19"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
@@ -6709,6 +6745,11 @@ md5.js@^1.3.4:
hash-base "^3.0.0"
inherits "^2.0.1"
+mdurl@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+ integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6942,15 +6983,17 @@ moment@2.x, moment@^2.21.0:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=
-monaco-editor-webpack-plugin@^1.5.4:
- version "1.5.4"
- resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.5.4.tgz#6781a130e3e1379bb8f4cd190132f4af6dcd2c16"
- integrity sha512-9YmWYQdZoAoZ1RLy/uvoDbCcb0EKy5O2qoMQn+UIVQxk+VTCXfJDgANczDIWko+UOzg0MY0P+sA8bl4XI14RJg==
+monaco-editor-webpack-plugin@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83"
+ integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==
+ dependencies:
+ "@types/webpack" "^4.4.19"
-monaco-editor@^0.14.3:
- version "0.14.3"
- resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.14.3.tgz#7cc4a4096a3821f52fea9b10489b527ef3034e22"
- integrity sha512-RhaO4xXmWn/p0WrkEOXe4PoZj6xOcvDYjoAh0e1kGUrQnP1IOpc0m86Ceuaa2CLEMDINqKijBSmqhvBQnsPLHQ==
+monaco-editor@^0.15.6:
+ version "0.15.6"
+ resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483"
+ integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg==
mousetrap@^1.4.6:
version "1.4.6"
@@ -7137,21 +7180,21 @@ node-releases@^1.1.3:
dependencies:
semver "^5.3.0"
-nodemon@^1.18.4:
- version "1.18.4"
- resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.4.tgz#873f65fdb53220eb166180cf106b1354ac5d714d"
- integrity sha512-hyK6vl65IPnky/ee+D3IWvVGgJa/m3No2/Xc/3wanS6Ce1MWjCzH6NnhPJ/vZM+6JFym16jtHx51lmCMB9HDtg==
+nodemon@^1.18.9:
+ version "1.18.9"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.9.tgz#90b467efd3b3c81b9453380aeb2a2cba535d0ead"
+ integrity sha512-oj/eEVTEI47pzYAjGkpcNw0xYwTl4XSTUQv2NPQI6PpN3b75PhpuYk3Vb3U80xHCyM2Jm+1j68ULHXl4OR3Afw==
dependencies:
- chokidar "^2.0.2"
+ chokidar "^2.0.4"
debug "^3.1.0"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
- pstree.remy "^1.1.0"
+ pstree.remy "^1.1.6"
semver "^5.5.0"
supports-color "^5.2.0"
touch "^3.1.0"
undefsafe "^2.0.2"
- update-notifier "^2.3.0"
+ update-notifier "^2.5.0"
nopt@3.x:
version "3.0.6"
@@ -7420,6 +7463,11 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
+orderedmap@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba"
+ integrity sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo=
+
original@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
@@ -7707,13 +7755,6 @@ path-type@^3.0.0:
dependencies:
pify "^3.0.0"
-pause-stream@0.0.11:
- version "0.0.11"
- resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
- integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=
- dependencies:
- through "~2.3"
-
pbkdf2@^3.0.3:
version "3.0.14"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
@@ -7963,6 +8004,122 @@ prompts@^0.1.9:
kleur "^2.0.1"
sisteransi "^0.1.1"
+prosemirror-commands@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz#e5a2ba821e29ea7065c88277fe2c3d7f6b0b9d37"
+ integrity sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-dropcursor@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz#c60ed1ed6c58804a06a75db06a0d993b087b7622"
+ integrity sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+ prosemirror-view "^1.1.0"
+
+prosemirror-gapcursor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz#acc6537fc5a35e9b38966f91a199a382dfc715c4"
+ integrity sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==
+ dependencies:
+ prosemirror-keymap "^1.0.0"
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-view "^1.0.0"
+
+prosemirror-history@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.0.3.tgz#5fb8591adfc272afaaf0b41bec64ee7d9522a118"
+ integrity sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q==
+ dependencies:
+ prosemirror-state "^1.2.2"
+ prosemirror-transform "^1.0.0"
+ rope-sequence "^1.2.0"
+
+prosemirror-inputrules@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz#f63305fd966379f218e82ca76a2a9b328b66dc7b"
+ integrity sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2"
+ integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ w3c-keyname "^1.1.8"
+
+prosemirror-markdown@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.3.0.tgz#a100d14c27da7d8fb70818230d786898eeadb7fa"
+ integrity sha512-76l3yLB/suy6sA7LpzRJvRRWkHtKwOTpgWVNwmlIAIIZJeMypWSPldT/gFyIG604eyXEPZitnx+j80Y2DpbnUQ==
+ dependencies:
+ markdown-it "^8.4.2"
+ prosemirror-model "^1.0.0"
+
+prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.6.4.tgz#2ac37a629448a7dbfd1635450e2fdd63c3450d7d"
+ integrity sha512-C2ALle8fZsAza+6stUF9Gv28jH9XtpNeczb33bowGlnb2cpNI4FZf1HHUyZjf6ou4cEvOlbt6fAYsT4NCKmlcQ==
+ dependencies:
+ orderedmap "^1.0.0"
+
+prosemirror-schema-list@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz#f216e0cf4809b6074aa27912449ac89897f1ae94"
+ integrity sha512-AiLIX6qm6PEeDtMCKZLcSLi55WXo1ls7DnRK+4hSkoi0IIzNdxGsRlecCd3MzEu//DVz3nAEh+zEmslyW+uk8g==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-state@^1.0.0, prosemirror-state@^1.2.1, prosemirror-state@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.2.tgz#8df26d95fd6fd327c0f9984a760e84d863204154"
+ integrity sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-tables@^0.7.10, prosemirror-tables@^0.7.9:
+ version "0.7.10"
+ resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.7.10.tgz#4b0f623422b4b8f84cdc9c559f8a87579846b3ba"
+ integrity sha512-VIu7UGS9keYEHs0Y6AEOTGbNE9QI2rL1OKng4vV6yoTshW/lYcb+s3hGXI12i+WLMjDVm7ujhfdWrpKpvFZOkQ==
+ dependencies:
+ prosemirror-keymap "^1.0.0"
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.0.0"
+
+prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz#28cfdf1f9ee514edc40466be7b7db39eed545fdf"
+ integrity sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==
+ dependencies:
+ prosemirror-model "^1.0.0"
+
+prosemirror-utils@^0.7.5:
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.7.5.tgz#11b477647b672ec8f10679ab298a5823dad6457a"
+ integrity sha512-F+63BUiBkUQb1S07c3rGHXjE4MDaZ5OjsNhmaO7eDdSh1lUNORTJJHrvlFEZKnLM7ChoDDXTIKhWNQwnCssQfA==
+
+prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.6.8:
+ version "1.6.8"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.6.8.tgz#33fc1a6e2731633e5d6dc1af1967378f15810b74"
+ integrity sha512-YWX3rfji77xsU5EErt4ZoecVytYW9/4oHBYhV1MUHGMYIcppe+QZEBgRlyPMBUuu0lxdZX4m3sq7fCsDvv/MlQ==
+ dependencies:
+ prosemirror-model "^1.1.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -7981,13 +8138,6 @@ prr@~1.0.1:
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
-ps-tree@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
- integrity sha1-tCGyQUDWID8e08dplrRCewjowBQ=
- dependencies:
- event-stream "~3.3.0"
-
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -7998,12 +8148,10 @@ psl@^1.1.24:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
-pstree.remy@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.0.tgz#f2af27265bd3e5b32bbfcc10e80bac55ba78688b"
- integrity sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==
- dependencies:
- ps-tree "^1.1.0"
+pstree.remy@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.6.tgz#73a55aad9e2d95814927131fbf4dc1b62d259f47"
+ integrity sha512-NdF35+QsqD7EgNEI5mkI/X+UwaxVEbQaz9f4IooEmMUv6ZPmlTQYGjBPJGgrlzNdjSvIy4MWMg6Q6vCgBO2K+w==
public-encrypt@^4.0.0:
version "4.0.0"
@@ -8565,6 +8713,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0"
inherits "^2.0.1"
+rope-sequence@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce"
+ integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=
+
rsvp@^3.3.3:
version "3.6.2"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
@@ -9105,13 +9258,6 @@ split-string@^3.0.1, split-string@^3.0.2:
dependencies:
extend-shallow "^3.0.0"
-split@0.3:
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
- integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=
- dependencies:
- through "2"
-
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@@ -9193,13 +9339,6 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"
-stream-combiner@~0.0.4:
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
- integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=
- dependencies:
- duplexer "~0.1.1"
-
stream-each@^1.1.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
@@ -9493,7 +9632,7 @@ through2@^2.0.0:
readable-stream "^2.1.5"
xtend "~4.0.1"
-through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
+through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@@ -9527,6 +9666,57 @@ tiny-emitter@^2.0.0:
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==
+tiptap-commands@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.4.0.tgz#0cfb3ac138ee3099de56114cb119abd841fbcbe7"
+ integrity sha512-ytO8jFXgufK5DziamTaVojzUTolWvL4m2xNXaLkAVJYy9CWXruMK7avqeLoFYPI4GZlhleMn5i4gzYTbD7e2jA==
+ dependencies:
+ prosemirror-commands "^1.0.7"
+ prosemirror-inputrules "^1.0.1"
+ prosemirror-schema-list "^1.0.1"
+ prosemirror-state "^1.2.2"
+ tiptap-utils "^1.1.1"
+
+tiptap-extensions@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.8.0.tgz#3067620a024f1a9e5fae4450790b143d7ebe4394"
+ integrity sha512-1JN9uk5QnA7DTID1j07gIBEqeOnRd6lwZ5rx/zqWXJLyreZu8VDPvP939tfP41GskO4oicGlhmsQ0aEnA5QYDw==
+ dependencies:
+ lowlight "^1.11.0"
+ prosemirror-history "^1.0.3"
+ prosemirror-state "^1.2.2"
+ prosemirror-tables "^0.7.10"
+ prosemirror-utils "^0.7.5"
+ prosemirror-view "^1.6.8"
+ tiptap "^1.8.0"
+ tiptap-commands "^1.4.0"
+
+tiptap-utils@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.1.1.tgz#e7aad3e84eb35f7abed704d15da0420029789d0d"
+ integrity sha512-yPIWwLFaL5a0GC7fcO7aoPlASnH3wOUQex0IlepNWbDCNycSL8shXhVx0HMN/tCnlp943zw1bwcYzpTW3wA4tw==
+ dependencies:
+ prosemirror-model "^1.6.4"
+ prosemirror-state "^1.2.2"
+ prosemirror-tables "^0.7.9"
+ prosemirror-utils "^0.7.5"
+
+tiptap@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.8.0.tgz#c671188075ffa5ee4f86470f95818fd9ce6f1040"
+ integrity sha512-zIcVY8U1Wgj4bg3R4pX5a2BCpZUw/dTCh259VZ9g5MtClnzdLW2XpKCcwqfa9iUBEs6MCPSnB3t8jGRtGciHJg==
+ dependencies:
+ prosemirror-commands "^1.0.7"
+ prosemirror-dropcursor "^1.1.1"
+ prosemirror-gapcursor "^1.0.3"
+ prosemirror-inputrules "^1.0.1"
+ prosemirror-keymap "^1.0.1"
+ prosemirror-model "^1.6.4"
+ prosemirror-state "^1.2.1"
+ prosemirror-view "^1.6.8"
+ tiptap-commands "^1.4.0"
+ tiptap-utils "^1.1.1"
+
tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9678,6 +9868,11 @@ typescript@^2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
+ integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==
+
uglify-js@^3.1.4:
version "3.4.9"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
@@ -9802,15 +9997,16 @@ upath@^1.0.5:
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==
-update-notifier@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451"
- integrity sha1-TognpruRUUCrCTVZ1wFOPruDdFE=
+update-notifier@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
+ integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
dependencies:
boxen "^1.2.1"
chalk "^2.0.1"
configstore "^3.0.0"
import-lazy "^2.1.0"
+ is-ci "^1.0.10"
is-installed-globally "^0.1.0"
is-npm "^1.0.0"
latest-version "^3.0.0"
@@ -10081,6 +10277,11 @@ w3c-hr-time@^1.0.1:
dependencies:
browser-process-hrtime "^0.1.2"
+w3c-keyname@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c"
+ integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==
+
walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
@@ -10221,17 +10422,17 @@ webpack-stats-plugin@^0.2.1:
resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595"
integrity sha512-OYMZLpZrK/qLA79NE4kC4DCt85h/5ipvWJcsefKe9MMw0qU4/ck/IJg+4OmWA+5EfrZZpHXDq92IptfYDWVfkw==
-webpack@^4.28.1:
- version "4.28.1"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.28.1.tgz#d0e2856e75d1224b170bf16c30b6ca9b75f0d958"
- integrity sha512-qAS7BFyS5iuOZzGJxyDXmEI289h7tVNtJ5XMxf6Tz55J2riOyH42uaEsWF0F32TRaI+54SmI6qRgHM3GzsZ+sQ==
+webpack@^4.29.0:
+ version "4.29.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.0.tgz#f2cfef83f7ae404ba889ff5d43efd285ca26e750"
+ integrity sha512-pxdGG0keDBtamE1mNvT5zyBdx+7wkh6mh7uzMOo/uRQ/fhsdj5FXkh/j5mapzs060forql1oXqXN9HJGju+y7w==
dependencies:
"@webassemblyjs/ast" "1.7.11"
"@webassemblyjs/helper-module-context" "1.7.11"
"@webassemblyjs/wasm-edit" "1.7.11"
"@webassemblyjs/wasm-parser" "1.7.11"
- acorn "^5.6.2"
- acorn-dynamic-import "^3.0.0"
+ acorn "^6.0.5"
+ acorn-dynamic-import "^4.0.0"
ajv "^6.1.0"
ajv-keywords "^3.1.0"
chrome-trace-event "^1.0.0"
@@ -10568,10 +10769,10 @@ yargs@^12.0.4:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
-yarn-deduplicate@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.0.5.tgz#e56016f1c29e77e323f401ea838f5e8c7cdbfd42"
- integrity sha512-4nds6N7dxuXcfUZAVaSUVSlI4TvwEdMaZg/DRBf/KM3iFezNBdkhcTYptcwKaecAYAfVxx3g0Ex21kssSr8YsA==
+yarn-deduplicate@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.0.tgz#bdfdcc5a2473556c0232996424dfe039293f2f44"
+ integrity sha512-YTZzmzzUgDK7IllsKxgnTQ7zAGbTVnj3bnH3nxoqZ2dE0IY7NpaFpFYXR+BuBeDtxIgMhwJJvH1LTWm3k3fWpg==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
commander "^2.10.0"