summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/none-scheme-preview.pngbin0 -> 5971 bytes
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js1
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js14
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js20
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue9
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js10
-rw-r--r--app/assets/javascripts/diffs/components/app.vue6
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue22
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue1
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue12
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue8
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue9
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue10
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js2
-rw-r--r--app/assets/javascripts/diffs/store/utils.js11
-rw-r--r--app/assets/javascripts/dismissable_callout.js27
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue6
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js17
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue32
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue21
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue35
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue3
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue6
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue17
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js198
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js32
-rw-r--r--app/assets/javascripts/mr_notes/index.js2
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue20
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue38
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue37
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue73
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue33
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue8
-rw-r--r--app/assets/javascripts/notes/constants.js1
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js20
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js11
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/releases/index/index.js3
-rw-r--r--app/assets/javascripts/pages/users/user_overview_block.js15
-rw-r--r--app/assets/javascripts/persistent_user_callout.js34
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue4
-rw-r--r--app/assets/javascripts/releases/components/app.vue82
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue129
-rw-r--r--app/assets/javascripts/releases/index.js24
-rw-r--r--app/assets/javascripts/releases/store/actions.js37
-rw-r--r--app/assets/javascripts/releases/store/index.js14
-rw-r--r--app/assets/javascripts/releases/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/releases/store/mutations.js37
-rw-r--r--app/assets/javascripts/releases/store/state.js5
-rw-r--r--app/assets/javascripts/right_sidebar.js15
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue2
-rw-r--r--app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js3
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue74
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue60
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue136
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue17
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/actions.js17
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/index.js10
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js4
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/mutations.js18
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/state.js4
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss18
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/animations.scss22
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss7
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/highlight.scss7
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss5
-rw-r--r--app/assets/stylesheets/framework/layout.scss8
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss27
-rw-r--r--app/assets/stylesheets/framework/mobile.scss88
-rw-r--r--app/assets/stylesheets/framework/modal.scss8
-rw-r--r--app/assets/stylesheets/framework/selects.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss19
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss6
-rw-r--r--app/assets/stylesheets/highlight/none.scss242
-rw-r--r--app/assets/stylesheets/pages/builds.scss6
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss24
-rw-r--r--app/assets/stylesheets/pages/milestone.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss1
-rw-r--r--app/assets/stylesheets/pages/profile.scss11
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/health_check_controller.rb8
-rw-r--r--app/controllers/application_controller.rb9
-rw-r--r--app/controllers/clusters/clusters_controller.rb16
-rw-r--r--app/controllers/concerns/group_tree.rb4
-rw-r--r--app/controllers/concerns/invalid_utf8_error_handler.rb27
-rw-r--r--app/controllers/concerns/issuable_actions.rb10
-rw-r--r--app/controllers/concerns/lfs_request.rb6
-rw-r--r--app/controllers/concerns/preview_markdown.rb10
-rw-r--r--app/controllers/concerns/service_params.rb6
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb21
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb6
-rw-r--r--app/controllers/profiles/keys_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/git_http_controller.rb4
-rw-r--r--app/controllers/projects/imports_controller.rb10
-rw-r--r--app/controllers/projects/issues_controller.rb3
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/conflicts_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests_controller.rb12
-rw-r--r--app/controllers/projects/protected_branches_controller.rb4
-rw-r--r--app/controllers/projects/protected_refs_controller.rb4
-rw-r--r--app/controllers/projects/protected_tags_controller.rb4
-rw-r--r--app/controllers/projects/releases_controller.rb35
-rw-r--r--app/controllers/projects/settings/repository_controller.rb5
-rw-r--r--app/controllers/projects/snippets_controller.rb9
-rw-r--r--app/controllers/projects/tags/releases_controller.rb42
-rw-r--r--app/controllers/projects/tags_controller.rb19
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb8
-rw-r--r--app/finders/cluster_ancestors_finder.rb35
-rw-r--r--app/finders/concerns/finder_with_cross_project_access.rb30
-rw-r--r--app/finders/events_finder.rb45
-rw-r--r--app/finders/group_descendants_finder.rb6
-rw-r--r--app/finders/groups_finder.rb4
-rw-r--r--app/finders/releases_finder.rb14
-rw-r--r--app/helpers/appearances_helper.rb4
-rw-r--r--app/helpers/application_helper.rb7
-rw-r--r--app/helpers/application_settings_helper.rb17
-rw-r--r--app/helpers/blob_helper.rb35
-rw-r--r--app/helpers/ci_variables_helper.rb15
-rw-r--r--app/helpers/diff_helper.rb24
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb109
-rw-r--r--app/helpers/milestones_helper.rb23
-rw-r--r--app/helpers/projects_helper.rb32
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/snippets_helper.rb10
-rw-r--r--app/helpers/sorting_helper.rb23
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/helpers/workhorse_helper.rb5
-rw-r--r--app/mailers/emails/issues.rb4
-rw-r--r--app/mailers/emails/merge_requests.rb4
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/appearance.rb28
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/blob.rb10
-rw-r--r--app/models/blob_viewer/base.rb6
-rw-r--r--app/models/broadcast_message.rb37
-rw-r--r--app/models/ci/build.rb10
-rw-r--r--app/models/ci/pipeline.rb39
-rw-r--r--app/models/ci/pipeline_enums.rb10
-rw-r--r--app/models/ci/runner.rb5
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb2
-rw-r--r--app/models/clusters/applications/knative.rb13
-rw-r--r--app/models/clusters/applications/prometheus.rb7
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb1
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/concerns/avatarable.rb13
-rw-r--r--app/models/concerns/blob_like.rb2
-rw-r--r--app/models/concerns/cacheable_attributes.rb7
-rw-r--r--app/models/concerns/descendant.rb11
-rw-r--r--app/models/concerns/discussion_on_diff.rb21
-rw-r--r--app/models/concerns/enum_with_nil.rb3
-rw-r--r--app/models/concerns/has_ref.rb17
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/concerns/noteable.rb8
-rw-r--r--app/models/concerns/redis_cacheable.rb6
-rw-r--r--app/models/dashboard_group_milestone.rb21
-rw-r--r--app/models/dashboard_milestone.rb10
-rw-r--r--app/models/diff_note.rb13
-rw-r--r--app/models/diff_viewer/base.rb39
-rw-r--r--app/models/diff_viewer/image.rb2
-rw-r--r--app/models/diff_viewer/rich.rb2
-rw-r--r--app/models/diff_viewer/server_side.rb12
-rw-r--r--app/models/diff_viewer/simple.rb2
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/event.rb13
-rw-r--r--app/models/global_milestone.rb116
-rw-r--r--app/models/group.rb5
-rw-r--r--app/models/group_milestone.rb29
-rw-r--r--app/models/merge_request.rb36
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/models/namespace.rb17
-rw-r--r--app/models/note.rb12
-rw-r--r--app/models/note_diff_file.rb15
-rw-r--r--app/models/pool_repository.rb17
-rw-r--r--app/models/project.rb100
-rw-r--r--app/models/project_services/kubernetes_service.rb2
-rw-r--r--app/models/prometheus_metric.rb85
-rw-r--r--app/models/release.rb29
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb13
-rw-r--r--app/models/service.rb6
-rw-r--r--app/models/snippet.rb6
-rw-r--r--app/models/suggestion.rb55
-rw-r--r--app/models/todo.rb5
-rw-r--r--app/models/user.rb11
-rw-r--r--app/policies/concerns/clusterable_actions.rb14
-rw-r--r--app/policies/group_policy.rb13
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/project_policy.rb14
-rw-r--r--app/policies/release_policy.rb5
-rw-r--r--app/policies/suggestion_policy.rb11
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/clusters/cluster_presenter.rb50
-rw-r--r--app/serializers/diff_file_entity.rb1
-rw-r--r--app/serializers/diff_line_entity.rb2
-rw-r--r--app/serializers/diff_viewer_entity.rb3
-rw-r--r--app/serializers/entity_date_helper.rb16
-rw-r--r--app/serializers/environment_entity.rb16
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb106
-rw-r--r--app/serializers/issuable_sidebar_extras_entity.rb (renamed from app/serializers/issuable_sidebar_entity.rb)4
-rw-r--r--app/serializers/issuable_sidebar_todo_entity.rb11
-rw-r--r--app/serializers/issue_board_entity.rb4
-rw-r--r--app/serializers/issue_serializer.rb6
-rw-r--r--app/serializers/issue_sidebar_basic_entity.rb6
-rw-r--r--app/serializers/issue_sidebar_extras_entity.rb (renamed from app/serializers/issue_sidebar_entity.rb)2
-rw-r--r--app/serializers/merge_request_basic_entity.rb2
-rw-r--r--app/serializers/merge_request_basic_serializer.rb5
-rw-r--r--app/serializers/merge_request_serializer.rb9
-rw-r--r--app/serializers/merge_request_sidebar_basic_entity.rb11
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/serializers/note_entity.rb1
-rw-r--r--app/serializers/suggestion_entity.rb17
-rw-r--r--app/services/ci/create_pipeline_service.rb3
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb8
-rw-r--r--app/services/commits/tag_service.rb3
-rw-r--r--app/services/create_release_service.rb30
-rw-r--r--app/services/deploy_keys/create_service.rb2
-rw-r--r--app/services/git_push_service.rb3
-rw-r--r--app/services/git_tag_push_service.rb3
-rw-r--r--app/services/groups/nested_create_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb2
-rw-r--r--app/services/groups/update_service.rb4
-rw-r--r--app/services/issuable/common_system_notes_service.rb21
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/promote_service.rb8
-rw-r--r--app/services/members/base_service.rb6
-rw-r--r--app/services/members/destroy_service.rb8
-rw-r--r--app/services/members/update_service.rb9
-rw-r--r--app/services/merge_requests/build_service.rb24
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/notes/create_service.rb1
-rw-r--r--app/services/notes/update_service.rb11
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/preview_markdown_service.rb8
-rw-r--r--app/services/projects/after_rename_service.rb1
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb36
-rw-r--r--app/services/projects/transfer_service.rb7
-rw-r--r--app/services/projects/update_service.rb4
-rw-r--r--app/services/releases/concerns.rb48
-rw-r--r--app/services/releases/create_service.rb54
-rw-r--r--app/services/releases/destroy_service.rb25
-rw-r--r--app/services/releases/update_service.rb32
-rw-r--r--app/services/suggestions/apply_service.rb54
-rw-r--r--app/services/suggestions/create_service.rb56
-rw-r--r--app/services/tags/create_service.rb7
-rw-r--r--app/services/tags/destroy_service.rb10
-rw-r--r--app/services/update_release_service.rb28
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb2
-rw-r--r--app/services/users/update_service.rb10
-rw-r--r--app/views/admin/appearances/_form.html.haml6
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml7
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml4
-rw-r--r--app/views/ci/variables/_content.html.haml4
-rw-r--r--app/views/ci/variables/_header.html.haml11
-rw-r--r--app/views/ci/variables/_index.html.haml5
-rw-r--r--app/views/ci/variables/_variable_row.html.haml6
-rw-r--r--app/views/clusters/clusters/_buttons.html.haml8
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml4
-rw-r--r--app/views/clusters/clusters/_empty_state.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml4
-rw-r--r--app/views/clusters/clusters/index.html.haml7
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/dashboard/activity.html.haml3
-rw-r--r--app/views/dashboard/groups/index.html.haml2
-rw-r--r--app/views/dashboard/issues.html.haml2
-rw-r--r--app/views/dashboard/merge_requests.html.haml2
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/events/_events.html.haml5
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/index.html.haml2
-rw-r--r--app/views/explore/projects/starred.html.haml2
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml8
-rw-r--r--app/views/issues/_issues_calendar.ics.ruby2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml3
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml26
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml7
-rw-r--r--app/views/notify/_note_email.html.haml10
-rw-r--r--app/views/notify/_note_email.text.erb2
-rw-r--r--app/views/notify/changed_milestone_email.html.haml (renamed from app/views/notify/changed_milestone_issue_email.html.haml)2
-rw-r--r--app/views/notify/changed_milestone_email.text.erb1
-rw-r--r--app/views/notify/changed_milestone_issue_email.text.erb1
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.html.haml3
-rw-r--r--app/views/notify/changed_milestone_merge_request_email.text.erb1
-rw-r--r--app/views/profiles/show.html.haml38
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml3
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/_md_preview.html.haml12
-rw-r--r--app/views/projects/blob/_editor.html.haml4
-rw-r--r--app/views/projects/blob/_markdown_buttons.html.haml13
-rw-r--r--app/views/projects/buttons/_clone.html.haml17
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/diffs/_render_error.html.haml6
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml36
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml6
-rw-r--r--app/views/projects/releases/index.html.haml5
-rw-r--r--app/views/projects/services/prometheus/_metrics.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml8
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/releases/edit.html.haml (renamed from app/views/projects/releases/edit.html.haml)0
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml12
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml10
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml8
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml165
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml51
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml18
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml15
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml3
-rw-r--r--app/views/shared/issuable/nav_links/_all.html.haml2
-rw-r--r--app/views/shared/labels/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml14
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml11
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/users/_overview.html.haml30
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/cluster_configure_worker.rb (renamed from app/workers/cluster_platform_configure_worker.rb)2
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb23
-rw-r--r--app/workers/object_pool/destroy_worker.rb16
-rw-r--r--app/workers/post_receive.rb20
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb6
391 files changed, 4296 insertions, 1511 deletions
diff --git a/app/assets/images/none-scheme-preview.png b/app/assets/images/none-scheme-preview.png
new file mode 100644
index 00000000000..2eb6bf96671
--- /dev/null
+++ b/app/assets/images/none-scheme-preview.png
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index e2740981a4b..d1396b6c4bc 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -25,9 +25,11 @@ const Api = {
userStatusPath: '/api/:version/users/:id/status',
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
+ applySuggestionPath: '/api/:version/suggestions/:id/apply',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
+ releasesPath: '/api/:version/projects/:id/releases',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -185,6 +187,12 @@ const Api = {
});
},
+ applySuggestion(id) {
+ const url = Api.buildUrl(Api.applySuggestionPath).replace(':id', encodeURIComponent(id));
+
+ return axios.put(url);
+ },
+
commitPipelines(projectId, sha) {
const encodedProjectId = projectId
.split('/')
@@ -300,6 +308,12 @@ const Api = {
});
},
+ releases(id) {
+ const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index fa9b2c9f755..bef1553703b 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -8,6 +8,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g r', () => findAndFollowLink('.shortcuts-project-releases'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 9f547471170..5f64175362d 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -16,13 +16,25 @@ export default () => {
const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
+ const isMarkdown = editBlobForm.data('is-markdown');
const commitButton = $('.js-commit-button');
+ const cancelLink = $('.btn.btn-cancel');
+
+ cancelLink.on('click', () => {
+ window.onbeforeunload = null;
+ });
commitButton.on('click', () => {
window.onbeforeunload = null;
});
- new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId);
+ new EditBlob({
+ assetsPath: `${urlRoot}${assetsPath}`,
+ filePath,
+ currentAction,
+ projectId,
+ isMarkdown,
+ });
new NewCommitForm(editBlobForm);
// returning here blocks page navigation
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 6e19548eed2..011898a5e7a 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -6,22 +6,31 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
+import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
export default class EditBlob {
- constructor(assetsPath, aceMode, currentAction, projectId) {
- this.configureAceEditor(aceMode, assetsPath);
+ // The options object has:
+ // assetsPath, filePath, currentAction, projectId, isMarkdown
+ constructor(options) {
+ this.options = options;
+ this.configureAceEditor();
this.initModePanesAndLinks();
this.initSoftWrap();
- this.initFileSelectors(currentAction, projectId);
+ this.initFileSelectors();
}
- configureAceEditor(filePath, assetsPath) {
+ configureAceEditor() {
+ const { filePath, assetsPath, isMarkdown } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor');
+ if (isMarkdown) {
+ addEditorMarkdownListeners(this.editor);
+ }
+
// This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity;
@@ -32,7 +41,8 @@ export default class EditBlob {
}
}
- initFileSelectors(currentAction, projectId) {
+ initFileSelectors() {
+ const { currentAction, projectId } = this.options;
this.fileTemplateMediator = new TemplateSelectorMediator({
currentAction,
editor: this.editor,
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index e038198e6f0..9c4c6632976 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -3,7 +3,12 @@ import dateFormat from 'dateformat';
import { GlTooltip } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
-import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+import {
+ getDayDifference,
+ getTimeago,
+ dateInWords,
+ parsePikadayDate,
+} from '~/lib/utils/datetime_utility';
export default {
components: {
@@ -54,7 +59,7 @@ export default {
return standardDateFormat;
},
issueDueDate() {
- return new Date(this.date);
+ return parsePikadayDate(this.date);
},
timeDifference() {
const today = new Date();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 08408eb0b52..defd857b92c 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -45,7 +45,7 @@ export default {
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
- <aside class="svg-content"><img :src="emptyStateSvg" /></aside>
+ <aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index ee0f7cda189..5b20fa141cd 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -36,7 +36,9 @@ export default class VariableList {
},
protected: {
selector: '.js-ci-variable-input-protected',
- default: 'false',
+ // use `attr` instead of `data` as we don't want the value to be
+ // converted. we need the value as a string.
+ default: $('.js-ci-variable-input-protected').attr('data-default'),
},
environment_scope: {
// We can't use a `.js-` class here because
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index cf70a48f076..aff32d95db1 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,6 +1,6 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
@@ -67,7 +67,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
- initDismissableCallout('.js-cluster-security-warning');
+ Clusters.initDismissableCallout();
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(clusterType);
@@ -108,6 +108,12 @@ export default class Clusters {
});
}
+ static initDismissableCallout() {
+ const callout = document.querySelector('.js-cluster-security-warning');
+
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+ }
+
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index f0e82b1ed27..d4c1b07093d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -42,6 +42,11 @@ export default {
type: Object,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
changesEmptyStateIllustration: {
type: String,
required: false,
@@ -208,6 +213,7 @@ export default {
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
+ :help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
/>
</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 8da02ed0b7c..b9b1ee02697 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -129,7 +129,7 @@ export default {
</strong>
</div>
<div>
- <small class="commit-sha"> {{ version.truncated_commit_sha }} </small>
+ <small class="commit-sha"> {{ version.short_commit_sha }} </small>
</div>
<div>
<small>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 11cc4c09fed..ba6dcd63880 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
import NoteForm from '../../notes/components/note_form.vue';
@@ -17,12 +18,18 @@ export default {
NoteForm,
DiffDiscussions,
ImageDiffOverlay,
+ EmptyFileViewer,
},
props: {
diffFile: {
type: Object,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState({
@@ -38,6 +45,9 @@ export default {
isTextFile() {
return this.diffFile.viewer.name === 'text';
},
+ errorMessage() {
+ return this.diffFile.viewer.error;
+ },
diffFileCommentForm() {
return this.getCommentFormForDiffFile(this.diffFile.file_hash);
},
@@ -68,17 +78,20 @@ export default {
<template>
<div class="diff-content">
- <div class="diff-viewer">
+ <div v-if="!errorMessage" class="diff-viewer">
<template v-if="isTextFile">
+ <empty-file-viewer v-if="diffFile.empty" />
<inline-diff-view
- v-if="isInlineView"
+ v-else-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines || []"
+ :help-page-path="helpPagePath"
/>
<parallel-diff-view
- v-if="isParallelView"
+ v-else-if="isParallelView"
:diff-file="diffFile"
:diff-lines="diffFile.parallel_diff_lines || []"
+ :help-page-path="helpPagePath"
/>
</template>
<diff-viewer
@@ -119,5 +132,8 @@ export default {
</div>
</diff-viewer>
</div>
+ <div v-else class="diff-viewer">
+ <div class="nothing-here-block" v-html="errorMessage"></div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index bee29b04e92..b2021cd6061 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -13,6 +13,11 @@ export default {
type: Array,
required: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
shouldCollapseDiscussions: {
type: Boolean,
required: false,
@@ -23,6 +28,11 @@ export default {
required: false,
default: false,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
...mapActions(['toggleDiscussion']),
@@ -72,6 +82,8 @@ export default {
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
+ :line="line"
+ :help-page-path="helpPagePath"
@noteDeleted="deleteNoteHandler"
>
<span v-if="renderAvatarBadge" slot="avatar-badge" class="badge badge-pill">
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index bed29efb253..449f7007077 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -23,6 +23,11 @@ export default {
type: Boolean,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -164,6 +169,7 @@ export default {
v-if="!isCollapsed && file.renderIt"
:class="{ hidden: isCollapsed || file.too_large }"
:diff-file="file"
+ :help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed">
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 9fd02acbd6e..e7569ba7b84 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -94,6 +94,7 @@ export default {
ref="noteForm"
:is-editing="true"
:line-code="line.line_code"
+ :line="line"
save-button-title="Comment"
class="diff-comment-form"
@cancelForm="handleCancelCommentForm"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index aa40b24950a..814ee0b7c02 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -16,6 +16,11 @@ export default {
type: String,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
className() {
@@ -38,7 +43,12 @@ export default {
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes_content" colspan="3">
<div class="content">
- <diff-discussions v-if="line.discussions.length" :discussions="line.discussions" />
+ <diff-discussions
+ v-if="line.discussions.length"
+ :line="line"
+ :discussions="line.discussions"
+ :help-page-path="helpPagePath"
+ />
<diff-line-note-form
v-if="line.hasForm"
:diff-file-hash="diffFileHash"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 6a0ce760e6d..e781397214d 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -17,6 +17,11 @@ export default {
type: Array,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapGetters('diffs', ['commitId']),
@@ -44,9 +49,10 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-comment-row
- :key="`icr-${index}`"
+ :key="`icr-${line.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
+ :help-page-path="helpPagePath"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index b98463d3dd3..a65cf025cde 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -20,6 +20,11 @@ export default {
type: Number,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
hasExpandedDiscussionOnLeft() {
@@ -87,6 +92,8 @@ export default {
<diff-discussions
v-if="line.left.discussions.length"
:discussions="line.left.discussions"
+ :line="line.left"
+ :help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
@@ -102,6 +109,8 @@ export default {
<diff-discussions
v-if="line.right.discussions.length"
:discussions="line.right.discussions"
+ :line="line.right"
+ :help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 9a6e0e82529..1bf693380db 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -17,6 +17,11 @@ export default {
type: Array,
required: true,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapGetters('diffs', ['commitId']),
@@ -38,17 +43,18 @@ export default {
<tbody>
<template v-for="(line, index) in diffLines">
<parallel-diff-table-row
- :key="index"
+ :key="line.line_code"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
/>
<parallel-diff-comment-row
- :key="`dcr-${index}`"
+ :key="`dcr-${line.line_code || index}`"
:line="line"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
+ :help-page-path="helpPagePath"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 915cacb374f..b130cedc24c 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -16,6 +16,7 @@ export default function initDiffsApp(store) {
return {
endpoint: dataset.endpoint,
projectPath: dataset.projectPath,
+ helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
};
@@ -31,6 +32,7 @@ export default function initDiffsApp(store) {
endpoint: this.endpoint,
currentUser: this.currentUser,
projectPath: this.projectPath,
+ helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
},
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 2ea884d1293..ed4203cf5e0 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -138,7 +138,7 @@ export default {
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
- mapDiscussions(line),
+ lineCheck(line) ? mapDiscussions(line) : line,
);
}
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index cbaa0e26395..2fe20551642 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -196,6 +196,15 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
+function getLineCode({ left, right }, index) {
+ if (left && left.line_code) {
+ return left.line_code;
+ } else if (right && right.line_code) {
+ return right.line_code;
+ }
+ return index;
+}
+
// This prepares and optimizes the incoming diff data from the server
// by setting up incremental rendering and removing unneeded data
export function prepareDiffData(diffData) {
@@ -208,6 +217,8 @@ export function prepareDiffData(diffData) {
const linesLength = file.parallel_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.parallel_diff_lines[u];
+
+ line.line_code = getLineCode(line, u);
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
line.left.hasForm = false;
diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js
deleted file mode 100644
index 5185b019376..00000000000
--- a/app/assets/javascripts/dismissable_callout.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import Flash from '~/flash';
-
-export default function initDismissableCallout(alertSelector) {
- const alertEl = document.querySelector(alertSelector);
- if (!alertEl) {
- return;
- }
-
- const closeButtonEl = alertEl.getElementsByClassName('close')[0];
- const { dismissEndpoint, featureId } = closeButtonEl.dataset;
-
- closeButtonEl.addEventListener('click', () => {
- axios
- .post(dismissEndpoint, {
- feature_name: featureId,
- })
- .then(() => {
- $(alertEl).alert('close');
- })
- .catch(() => {
- Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
- });
- });
-}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index cd2f46fd07a..f44806d82a6 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -14,6 +14,7 @@ import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { CLUSTER_TYPE } from '~/clusters/constants';
/**
* Environment Item Component
@@ -85,6 +86,15 @@ export default {
},
/**
+ * Hide group cluster features which are not currently implemented.
+ *
+ * @returns {Boolean}
+ */
+ disableGroupClusterFeatures() {
+ return this.model && this.model.cluster_type === CLUSTER_TYPE.GROUP;
+ },
+
+ /**
* Returns whether the environment can be stopped.
*
* @returns {Boolean}
@@ -547,6 +557,7 @@ export default {
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
+ :disabled="disableGroupClusterFeatures"
/>
<rollback-component
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 83727caad16..6d74d136a94 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -19,6 +19,11 @@ export default {
required: false,
default: '',
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
title() {
@@ -33,6 +38,7 @@ export default {
:title="title"
:aria-label="title"
:href="terminalPath"
+ :class="{ disabled: disabled }"
class="btn terminal-button d-none d-sm-none d-md-block"
>
<icon name="terminal" />
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index c14eb936930..8178821be3d 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -256,7 +256,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Milestones.template;
+ tmpl = GfmAutoComplete.Milestones.templateFunction(value.title);
}
return tmpl;
},
@@ -323,7 +323,7 @@ class GfmAutoComplete {
searchKey: 'search',
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
- let tmpl = GfmAutoComplete.Labels.template;
+ let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title);
if (GfmAutoComplete.isLoading(value)) {
tmpl = GfmAutoComplete.Loading.template;
}
@@ -588,9 +588,11 @@ GfmAutoComplete.Members = {
},
};
GfmAutoComplete.Labels = {
- template:
- // eslint-disable-next-line no-template-curly-in-string
- '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
+ templateFunction(color, title) {
+ return `<li><span class="dropdown-label-box" style="background: ${_.escape(
+ color,
+ )}"></span> ${_.escape(title)}</li>`;
+ },
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
@@ -600,8 +602,9 @@ GfmAutoComplete.Issues = {
};
// Milestones
GfmAutoComplete.Milestones = {
- // eslint-disable-next-line no-template-curly-in-string
- template: '<li>${title}</li>',
+ templateFunction(title) {
+ return `<li>${_.escape(title)}</li>`;
+ },
};
GfmAutoComplete.Loading = {
template:
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index 309b7427b9e..0bce860df91 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -28,27 +28,29 @@ export default {
</script>
<template>
<div class="block">
- <div class="title">{{ s__('Job|Job artifacts') }}</div>
+ <div class="title font-weight-bold">{{ s__('Job|Job artifacts') }}</div>
- <p v-if="isExpired" class="js-artifacts-removed build-detail-row">
- {{ s__('Job|The artifacts were removed') }}
+ <p
+ v-if="isExpired || willExpire"
+ :class="{
+ 'js-artifacts-removed': isExpired,
+ 'js-artifacts-will-be-removed': willExpire,
+ }"
+ class="build-detail-row"
+ >
+ <span v-if="isExpired">{{ s__('Job|The artifacts were removed') }}</span>
+ <span v-if="willExpire">{{ s__('Job|The artifacts will be removed') }}</span>
+ <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
</p>
- <p v-else-if="willExpire" class="js-artifacts-will-be-removed build-detail-row">
- {{ s__('Job|The artifacts will be removed in') }}
- </p>
-
- <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
-
- <div class="btn-group d-flex" role="group">
+ <div class="btn-group d-flex prepend-top-10" role="group">
<gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
class="js-keep-artifacts btn btn-sm btn-default"
data-method="post"
+ >{{ s__('Job|Keep') }}</gl-link
>
- {{ s__('Job|Keep') }}
- </gl-link>
<gl-link
v-if="artifact.download_path"
@@ -56,17 +58,15 @@ export default {
class="js-download-artifacts btn btn-sm btn-default"
download
rel="nofollow"
+ >{{ s__('Job|Download') }}</gl-link
>
- {{ s__('Job|Download') }}
- </gl-link>
<gl-link
v-if="artifact.browse_path"
:href="artifact.browse_path"
class="js-browse-artifacts btn btn-sm btn-default"
+ >{{ s__('Job|Browse') }}</gl-link
>
- {{ s__('Job|Browse') }}
- </gl-link>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
index 3b9c61bd48c..e0f55518eef 100644
--- a/app/assets/javascripts/jobs/components/commit_block.vue
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -31,12 +31,12 @@ export default {
block: !isLastBlock,
}"
>
- <p>
- {{ __('Commit') }}
+ <p class="append-bottom-5">
+ <span class="font-weight-bold">{{ __('Commit') }}</span>
- <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">{{
- commit.short_id
- }}</gl-link>
+ <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit">
+ {{ commit.short_id }}
+ </gl-link>
<clipboard-button
:text="commit.short_id"
@@ -44,11 +44,14 @@ export default {
css-class="btn btn-clipboard btn-transparent"
/>
- <gl-link v-if="mergeRequest" :href="mergeRequest.path" class="js-link-commit link-commit"
- >!{{ mergeRequest.iid }}</gl-link
- >
+ <span v-if="mergeRequest">
+ {{ __('in') }}
+ <gl-link :href="mergeRequest.path" class="js-link-commit link-commit"
+ >!{{ mergeRequest.iid }}</gl-link
+ >
+ </span>
</p>
- <p class="build-light-text append-bottom-0">{{ commit.title }}</p>
+ <p class="append-bottom-0">{{ commit.title }}</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 934ecd0e3ec..ad3e7dabc79 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -110,22 +110,20 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <div class="block">
- <strong class="inline prepend-top-8"> {{ job.name }} </strong>
+ <div class="block d-flex align-items-center">
+ <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4>
<gl-link
v-if="job.retry_path"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
+ >{{ __('Retry') }}</gl-link
>
- {{ __('Retry') }}
- </gl-link>
<gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
- class="js-terminal-link pull-right btn btn-primary
- btn-inverted visible-md-block visible-lg-block"
+ class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block"
target="_blank"
>
{{ __('Debug') }} <icon name="external-link" />
@@ -133,8 +131,7 @@ export default {
<gl-button
:aria-label="__('Toggle Sidebar')"
type="button"
- class="btn btn-blank gutter-toggle
- float-right d-block d-md-none js-sidebar-build-toggle"
+ class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
@click="toggleSidebar"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
@@ -145,25 +142,18 @@ export default {
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="js-new-issue btn btn-success btn-inverted"
+ >{{ __('New issue') }}</gl-link
>
- {{ __('New issue') }}
- </gl-link>
<gl-link
v-if="job.retry_path"
:href="job.retry_path"
class="js-retry-job btn btn-inverted-secondary"
data-method="post"
rel="nofollow"
+ >{{ __('Retry') }}</gl-link
>
- {{ __('Retry') }}
- </gl-link>
</div>
<div :class="{ block: renderBlock }">
- <p v-if="job.merge_request" class="build-detail-row js-job-mr">
- <span class="build-light-text"> {{ __('Merge Request:') }} </span>
- <gl-link :href="job.merge_request.path"> !{{ job.merge_request.iid }} </gl-link>
- </p>
-
<detail-row
v-if="job.duration"
:value="duration"
@@ -198,10 +188,10 @@ export default {
title="Coverage"
/>
<p v-if="job.tags.length" class="build-detail-row js-job-tags">
- <span class="build-light-text"> {{ __('Tags:') }} </span>
- <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary">
- {{ tag }}
- </span>
+ <span class="font-weight-bold">{{ __('Tags:') }}</span>
+ <span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{
+ tag
+ }}</span>
</p>
<div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group">
@@ -210,9 +200,8 @@ export default {
class="js-cancel-job btn btn-sm btn-default"
data-method="post"
rel="nofollow"
+ >{{ __('Cancel') }}</gl-link
>
- {{ __('Cancel') }}
- </gl-link>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index 77be295e802..b826007ec2c 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -34,8 +34,7 @@ export default {
</script>
<template>
<p class="build-detail-row">
- <span v-if="hasTitle" class="build-light-text"> {{ title }}: </span> {{ value }}
-
+ <span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }}
<span v-if="hasHelpURL" class="help-button float-right">
<gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow">
<i class="fa fa-question-circle" aria-hidden="true"></i>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 90482500bbf..7f79e92067f 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -38,11 +38,11 @@ export default {
<div class="block-last dropdown">
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
- {{ __('Pipeline') }}
- <a :href="pipeline.path" class="js-pipeline-path link-commit"> #{{ pipeline.id }} </a>
+ <span class="font-weight-bold">{{ __('Pipeline') }}</span>
+ <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a>
<template v-if="hasRef">
{{ __('from') }}
- <a :href="pipeline.ref.path" class="link-commit ref-name"> {{ pipeline.ref.name }} </a>
+ <a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a>
</template>
<button
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
index 3cd3b743108..997737b3e23 100644
--- a/app/assets/javascripts/jobs/components/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -43,23 +43,24 @@ export default {
<template>
<div class="build-widget block">
- <h4 class="title">{{ __('Trigger') }}</h4>
-
<p
v-if="trigger.short_token"
class="js-short-token"
- :class="{ 'append-bottom-0': !hasVariables }"
+ :class="{ 'append-bottom-5': hasVariables, 'append-bottom-0': !hasVariables }"
>
- <span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }}
+ <span class="font-weight-bold">{{ __('Trigger token:') }}</span> {{ trigger.short_token }}
</p>
<template v-if="hasVariables">
<p class="trigger-variables-btn-container">
- <span class="build-light-text"> {{ __('Variables:') }} </span>
+ <span class="font-weight-bold">{{ __('Trigger variables:') }}</span>
- <gl-button v-if="hasValues" class="group js-reveal-variables" @click="toggleValues">
- {{ getToggleButtonText }}
- </gl-button>
+ <gl-button
+ v-if="hasValues"
+ class="btn-sm group js-reveal-variables trigger-variables-btn"
+ @click="toggleValues"
+ >{{ getToggleButtonText }}</gl-button
+ >
</p>
<table class="js-build-variables trigger-build-variables">
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 3618c6af7e2..84a617acb42 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -8,6 +8,10 @@ function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
}
+function addBlockTags(blockTag, selected) {
+ return `${blockTag}\n${selected}\n${blockTag}`;
+}
+
function lineBefore(text, textarea) {
var split;
split = text
@@ -24,59 +28,130 @@ function lineAfter(text, textarea) {
.split('\n')[0];
}
+function editorBlockTagText(text, blockTag, selected, editor) {
+ const lines = text.split('\n');
+ const selectionRange = editor.getSelectionRange();
+ const shouldRemoveBlock =
+ lines[selectionRange.start.row - 1] === blockTag &&
+ lines[selectionRange.end.row + 1] === blockTag;
+
+ if (shouldRemoveBlock) {
+ if (blockTag !== null) {
+ // ace is globally defined
+ // eslint-disable-next-line no-undef
+ const { Range } = ace.require('ace/range');
+ const lastLine = lines[selectionRange.end.row + 1];
+ const rangeWithBlockTags = new Range(
+ lines[selectionRange.start.row - 1],
+ 0,
+ selectionRange.end.row + 1,
+ lastLine.length,
+ );
+ editor.getSelection().setSelectionRange(rangeWithBlockTags);
+ }
+ return selected;
+ }
+ return addBlockTags(blockTag, selected);
+}
+
function blockTagText(text, textArea, blockTag, selected) {
- const before = lineBefore(text, textArea);
- const after = lineAfter(text, textArea);
- if (before === blockTag && after === blockTag) {
+ const shouldRemoveBlock =
+ lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag;
+
+ if (shouldRemoveBlock) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
}
return selected;
- } else {
- return blockTag + '\n' + selected + '\n' + blockTag;
}
+ return addBlockTags(blockTag, selected);
}
-function moveCursor({ textArea, tag, positionBetweenTags, removedLastNewLine, select }) {
+function moveCursor({
+ textArea,
+ tag,
+ cursorOffset,
+ positionBetweenTags,
+ removedLastNewLine,
+ select,
+ editor,
+ editorSelectionStart,
+ editorSelectionEnd,
+}) {
var pos;
- if (!textArea.setSelectionRange) {
+ if (textArea && !textArea.setSelectionRange) {
return;
}
if (select && select.length > 0) {
- // calculate the part of the text to be selected
- const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
- const endPosition = startPosition + select.length;
- return textArea.setSelectionRange(startPosition, endPosition);
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (positionBetweenTags) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
+ if (textArea) {
+ // calculate the part of the text to be selected
+ const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
+ const endPosition = startPosition + select.length;
+ return textArea.setSelectionRange(startPosition, endPosition);
+ } else if (editor) {
+ editor.navigateLeft(tag.length - tag.indexOf(select));
+ editor.getSelection().selectAWord();
+ return;
}
+ }
+ if (textArea) {
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (positionBetweenTags) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
- if (removedLastNewLine) {
- pos -= 1;
- }
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ if (cursorOffset) {
+ pos -= cursorOffset;
+ }
- return textArea.setSelectionRange(pos, pos);
+ return textArea.setSelectionRange(pos, pos);
+ }
+ } else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
+ if (positionBetweenTags) {
+ editor.navigateLeft(tag.length);
+ }
}
}
-export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
+export function insertMarkdownText({
+ textArea,
+ text,
+ tag,
+ cursorOffset,
+ blockTag,
+ selected = '',
+ wrap,
+ select,
+ editor,
+}) {
var textToInsert,
selectedSplit,
startChar,
removedLastNewLine,
removedFirstNewLine,
currentLineEmpty,
- lastNewLine;
+ lastNewLine,
+ editorSelectionStart,
+ editorSelectionEnd;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
+ if (editor) {
+ const selectionRange = editor.getSelectionRange();
+
+ editorSelectionStart = selectionRange.start;
+ editorSelectionEnd = selectionRange.end;
+ }
+
// check for link pattern and selected text is an URL
// if so fill in the url part instead of the text part of the pattern.
if (tag === LINK_TAG_PATTERN) {
@@ -99,14 +174,27 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
}
// Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
+ if (textArea) {
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+ } else if (editor) {
+ if (editorSelectionStart.row !== editorSelectionEnd.row) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
}
selectedSplit = selected.split('\n');
- if (!wrap) {
+ if (editor && !wrap) {
+ lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
+
+ if (/^\s*$/.test(lastNewLine)) {
+ currentLineEmpty = true;
+ }
+ } else if (textArea && !wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty)
@@ -115,13 +203,19 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
}
}
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+ const isBeginning =
+ (textArea && textArea.selectionStart === 0) ||
+ (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
+
+ startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
- textToInsert = blockTagText(text, textArea, blockTag, selected);
+ textToInsert = editor
+ ? editorBlockTagText(text, blockTag, selected, editor)
+ : blockTagText(text, textArea, blockTag, selected);
} else {
textToInsert = selectedSplit
.map(function(val) {
@@ -150,24 +244,41 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
textToInsert += '\n';
}
- insertText(textArea, textToInsert);
+ if (editor) {
+ editor.insert(textToInsert);
+ } else {
+ insertText(textArea, textToInsert);
+ }
return moveCursor({
textArea,
tag: tag.replace(textPlaceholder, selected),
+ cursorOffset,
positionBetweenTags: wrap && selected.length === 0,
removedLastNewLine,
select,
+ editor,
+ editorSelectionStart,
+ editorSelectionEnd,
});
}
-function updateText({ textArea, tag, blockTag, wrap, select }) {
+function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
- selected = selectedText(text, textArea);
+ selected = selectedText(text, textArea) || tagContent;
$textArea.focus();
- return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select });
+ return insertMarkdownText({
+ textArea,
+ text,
+ tag,
+ cursorOffset,
+ blockTag,
+ selected,
+ wrap,
+ select,
+ });
}
export function addMarkdownListeners(form) {
@@ -178,10 +289,31 @@ export function addMarkdownListeners(form) {
return updateText({
textArea: $this.closest('.md-area').find('textarea'),
tag: $this.data('mdTag'),
+ cursorOffset: $this.data('mdCursorOffset'),
blockTag: $this.data('mdBlock'),
wrap: !$this.data('mdPrepend'),
select: $this.data('mdSelect'),
+ tagContent: $this.data('mdTagContent'),
+ });
+ });
+}
+
+export function addEditorMarkdownListeners(editor) {
+ $('.js-md')
+ .off('click')
+ .on('click', function(e) {
+ const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data();
+
+ insertMarkdownText({
+ tag: mdTag,
+ blockTag: mdBlock,
+ wrap: !mdPrepend,
+ select: mdSelect,
+ selected: editor.getSelectedText(),
+ text: editor.getValue(),
+ editor,
});
+ editor.focus();
});
}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index bb24a1acdb3..50ba14dfb2e 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -92,7 +92,11 @@ function queryTimeSeries(query, graphDrawData, lineStyle) {
if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
- shouldRenderLegend = false;
+ if (timeSeriesParsed.length > 0) {
+ shouldRenderLegend = false;
+ } else {
+ shouldRenderLegend = true;
+ }
} else {
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
@@ -101,19 +105,6 @@ function queryTimeSeries(query, graphDrawData, lineStyle) {
}
}
- if (!shouldRenderLegend) {
- if (!timeSeriesParsed[0].tracksLegend) {
- timeSeriesParsed[0].tracksLegend = [];
- }
- timeSeriesParsed[0].tracksLegend.push({
- max: maximumValue,
- average: accum / timeSeries.values.length,
- lineStyle,
- lineColor,
- metricTag,
- });
- }
-
const values = datesWithoutGaps.map(time => ({
time,
value: findByDate(timeSeries.values, time),
@@ -135,6 +126,19 @@ function queryTimeSeries(query, graphDrawData, lineStyle) {
shouldRenderLegend,
renderCanary,
});
+
+ if (!shouldRenderLegend) {
+ if (!timeSeriesParsed[0].tracksLegend) {
+ timeSeriesParsed[0].tracksLegend = [];
+ }
+ timeSeriesParsed[0].tracksLegend.push({
+ max: maximumValue,
+ average: accum / timeSeries.values.length,
+ lineStyle,
+ lineColor,
+ metricTag,
+ });
+ }
});
return timeSeriesParsed;
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 1c98683c597..e4d72eb8318 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -33,6 +33,7 @@ export default function initMrNotes() {
noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
+ helpPagePath: notesDataset.helpPagePath,
};
},
computed: {
@@ -71,6 +72,7 @@ export default function initMrNotes() {
notesData: this.notesData,
userData: this.currentUserData,
shouldShow: this.activeTab === 'show',
+ helpPagePath: this.helpPagePath,
},
});
},
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 86c114a761a..f5c410211b6 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -2,7 +2,11 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants';
+import {
+ DISCUSSION_FILTERS_DEFAULT_VALUE,
+ HISTORY_ONLY_FILTER_VALUE,
+ DISCUSSION_TAB_LABEL,
+} from '../constants';
export default {
components: {
@@ -23,6 +27,7 @@ export default {
return {
currentValue: this.selectedValue,
defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE,
+ displayFilters: true,
};
},
computed: {
@@ -32,6 +37,14 @@ export default {
return this.filters.find(filter => filter.value === this.currentValue);
},
},
+ created() {
+ if (window.mrTabs) {
+ const { eventHub, currentTab } = window.mrTabs;
+
+ eventHub.$on('MergeRequestTabChange', this.toggleFilters);
+ this.toggleFilters(currentTab);
+ }
+ },
mounted() {
this.toggleCommentsForm();
},
@@ -51,12 +64,15 @@ export default {
toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
+ toggleFilters(tab) {
+ this.displayFilters = tab === DISCUSSION_TAB_LABEL;
+ },
},
};
</script>
<template>
- <div class="discussion-filter-container d-inline-block align-bottom">
+ <div v-if="displayFilters" class="discussion-filter-container d-inline-block align-bottom">
<button
id="discussion-filter-dropdown"
ref="dropdownToggle"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index c0bee600181..bcf5d334da4 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,10 +1,12 @@
<script>
+import { mapActions } from 'vuex';
import $ from 'jquery';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
import autosave from '../mixins/autosave';
+import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
export default {
components: {
@@ -12,6 +14,7 @@ export default {
noteAwardsList,
noteAttachment,
noteForm,
+ Suggestions,
},
mixins: [autosave],
props: {
@@ -19,6 +22,11 @@ export default {
type: Object,
required: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
canEdit: {
type: Boolean,
required: true,
@@ -28,11 +36,22 @@ export default {
required: false,
default: false,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
noteBody() {
return this.note.note;
},
+ hasSuggestion() {
+ return this.note.suggestions && this.note.suggestions.length;
+ },
+ lineType() {
+ return this.line ? this.line.type : null;
+ },
},
mounted() {
this.renderGFM();
@@ -53,6 +72,7 @@ export default {
}
},
methods: {
+ ...mapActions(['submitSuggestion']),
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
@@ -62,19 +82,35 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelForm', shouldConfirm, isDirty);
},
+ applySuggestion({ suggestionId, flashContainer, callback }) {
+ const { discussion_id: discussionId, id: noteId } = this.note;
+
+ this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer, callback });
+ },
},
};
</script>
<template>
<div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body">
- <div class="note-text md" v-html="note.note_html"></div>
+ <suggestions
+ v-if="hasSuggestion && !isEditing"
+ :suggestions="note.suggestions"
+ :note-html="note.note_html"
+ :line-type="lineType"
+ :help-page-path="helpPagePath"
+ @apply="applySuggestion"
+ />
+ <div v-else class="note-text md" v-html="note.note_html"></div>
<note-form
v-if="isEditing"
ref="noteForm"
:is-editing="isEditing"
:note-body="noteBody"
:note-id="note.id"
+ :line="line"
+ :note="note"
+ :help-page-path="helpPagePath"
:markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 95164183ccb..e78596f8b52 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,4 +1,5 @@
<script>
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -53,6 +54,21 @@ export default {
required: false,
default: false,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ note: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -79,7 +95,8 @@ export default {
return '#';
},
markdownPreviewPath() {
- return this.getNoteableDataByProp('preview_note_path');
+ const notable = this.getNoteableDataByProp('preview_note_path');
+ return mergeUrlParams({ preview_suggestions: true }, notable);
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
@@ -93,6 +110,18 @@ export default {
isDisabled() {
return !this.updatedNoteBody.length || this.isSubmitting;
},
+ discussionNote() {
+ const discussionNote = this.discussion.id
+ ? this.getDiscussionLastNote(this.discussion)
+ : this.note;
+ return discussionNote || {};
+ },
+ canSuggest() {
+ return (
+ this.getNoteableData.can_receive_suggestion &&
+ (this.line && this.line.can_receive_suggestion)
+ );
+ },
},
watch: {
noteBody() {
@@ -171,7 +200,11 @@ export default {
:markdown-docs-path="markdownDocsPath"
:markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
+ :line="line"
+ :note="discussionNote"
+ :can-suggest="canSuggest"
:add-spacing-classes="false"
+ :help-page-path="helpPagePath"
>
<textarea
id="note_note"
@@ -193,7 +226,7 @@ export default {
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-success js-comment-button"
+ class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button"
@click="handleUpdate();"
>
{{ saveButtonTitle }}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 5c9a28b8512..7c3f5d00308 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -49,6 +49,11 @@ export default {
type: Object,
required: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
renderDiffFile: {
type: Boolean,
required: false,
@@ -64,6 +69,11 @@ export default {
required: false,
default: false,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
const { diff_discussion: isDiffDiscussion, resolved } = this.discussion;
@@ -168,31 +178,39 @@ export default {
commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
}
- let text = s__('MergeRequests|started a discussion');
+ const {
+ for_commit: isForCommit,
+ diff_discussion: isDiffDiscussion,
+ active: isActive,
+ } = this.discussion;
- if (this.discussion.for_commit) {
+ let text = s__('MergeRequests|started a discussion');
+ if (isForCommit) {
text = s__(
'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
);
- } else if (this.discussion.diff_discussion) {
- if (this.discussion.active) {
- text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}');
- } else {
- text = s__(
- 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
- );
- }
+ } else if (isDiffDiscussion && commitId) {
+ text = isActive
+ ? s__('MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}')
+ : s__(
+ 'MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
+ );
+ } else if (isDiffDiscussion) {
+ text = isActive
+ ? s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}')
+ : s__(
+ 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
+ );
}
- return sprintf(
- text,
- {
- commitId,
- linkStart,
- linkEnd,
- },
- false,
- );
+ return sprintf(text, { commitId, linkStart, linkEnd }, false);
+ },
+ diffLine() {
+ if (this.discussion.diff_discussion && this.discussion.truncated_diff_lines) {
+ return this.discussion.truncated_diff_lines.slice(-1)[0];
+ }
+
+ return this.line;
},
},
watch: {
@@ -357,8 +375,18 @@ Please check your network connection and try again.`;
<component
:is="componentName(initialDiscussion)"
:note="componentData(initialDiscussion)"
+ :line="line"
+ :help-page-path="helpPagePath"
@handleDeleteNote="deleteNoteHandler"
>
+ <note-edited-text
+ v-if="discussion.resolved"
+ slot="discussion-resolved-text"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
<toggle-replies-widget
@@ -373,6 +401,8 @@ Please check your network connection and try again.`;
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="line"
@handleDeleteNote="deleteNoteHandler"
/>
</template>
@@ -383,6 +413,8 @@ Please check your network connection and try again.`;
v-for="(note, index) in discussion.notes"
:key="note.id"
:note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="diffLine"
@handleDeleteNote="deleteNoteHandler"
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
@@ -390,7 +422,7 @@ Please check your network connection and try again.`;
</template>
</ul>
<div
- v-if="!isRepliesCollapsed"
+ v-if="!isRepliesCollapsed || !hasReplies"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
@@ -447,6 +479,7 @@ Please check your network connection and try again.`;
ref="noteForm"
:discussion="discussion"
:is-editing="false"
+ :line="diffLine"
save-button-title="Comment"
@handleFormUpdate="saveReply"
@cancelForm="cancelReplyForm"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index a17be51353e..4c02588127e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -27,6 +27,16 @@ export default {
type: Object,
required: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -185,7 +195,7 @@ export default {
:img-alt="author.name"
:img-size="40"
>
- <slot slot="avatar-badge" name="avatar-badge"> </slot>
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
</user-avatar-link>
</div>
<div class="timeline-content">
@@ -217,14 +227,19 @@ export default {
@handleResolve="resolveHandler"
/>
</div>
- <note-body
- ref="noteBody"
- :note="note"
- :can-edit="note.current_user.can_edit"
- :is-editing="isEditing"
- @handleFormUpdate="formUpdateHandler"
- @cancelForm="formCancelHandler"
- />
+ <div class="timeline-discussion-body">
+ <slot name="discussion-resolved-text"></slot>
+ <note-body
+ ref="noteBody"
+ :note="note"
+ :line="line"
+ :can-edit="note.current_user.can_edit"
+ :is-editing="isEditing"
+ :help-page-path="helpPagePath"
+ @handleFormUpdate="formUpdateHandler"
+ @cancelForm="formCancelHandler"
+ />
+ </div>
</div>
</timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 27f896cee35..f3fcfdfda05 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -49,6 +49,11 @@ export default {
required: false,
default: 0,
},
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -206,6 +211,7 @@ export default {
:key="discussion.id"
:discussion="discussion"
:render-diff-file="true"
+ :help-page-path="helpPagePath"
/>
</template>
</ul>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 72a8ff28466..f1b0b12bdce 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -57,7 +57,7 @@ export default {
tooltip-placement="bottom"
/>
</div>
- <button class="btn btn-link js-replies-text" type="button" @click="toggle">
+ <button class="btn btn-link js-replies-text qa-expand-replies" type="button" @click="toggle">
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button>
{{ __('Last reply by') }}
@@ -66,7 +66,11 @@ export default {
</a>
<time-ago-tooltip :time="lastReply.created_at" tooltip-placement="bottom" />
</template>
- <span v-else class="collapse-replies-btn js-collapse-replies" @click="toggle">
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies qa-collapse-replies"
+ @click="toggle"
+ >
<icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }}
</span>
</li>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 3147dc64c27..78d365fe94b 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -17,6 +17,7 @@ export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description';
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
+export const DISCUSSION_TAB_LABEL = 'show';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index 47a6f07cce2..237e70c0a4c 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Api from '~/api';
import VueResource from 'vue-resource';
import * as constants from '../constants';
@@ -44,4 +45,7 @@ export default {
toggleIssueState(endpoint, data) {
return Vue.http.put(endpoint, data);
},
+ applySuggestion(id) {
+ return Api.applySuggestion(id);
+ },
};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 4716ab52333..65f85314fa0 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -405,5 +405,25 @@ export const startTaskList = ({ dispatch }) =>
export const updateResolvableDiscussonsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
+export const submitSuggestion = (
+ { commit },
+ { discussionId, noteId, suggestionId, flashContainer, callback },
+) => {
+ service
+ .applySuggestion(suggestionId)
+ .then(() => {
+ commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId });
+ callback();
+ })
+ .catch(() => {
+ Flash(
+ __('Something went wrong while applying the suggestion. Please try again.'),
+ 'alert',
+ flashContainer,
+ );
+ callback();
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index b5fe8bdb1d3..887e6d22b06 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -20,6 +20,7 @@ export default () => ({
userData: {},
noteableData: {
current_user: {},
+ preview_note_path: 'path/to/preview',
},
commentsDisabled: false,
resolvableDiscussionsCount: 0,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 9c68ab67a8c..df943c155f4 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -16,6 +16,7 @@ export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
+export const APPLY_SUGGESTION = 'APPLY_SUGGESTION';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 39ff0ff73d7..8992454be2e 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -197,6 +197,17 @@ export default {
}
},
+ [types.APPLY_SUGGESTION](state, { noteId, discussionId, suggestionId }) {
+ const noteObj = utils.findNoteObjectById(state.discussions, discussionId);
+ const comment = utils.findNoteObjectById(noteObj.notes, noteId);
+
+ comment.suggestions = comment.suggestions.map(suggestion => ({
+ ...suggestion,
+ applied: suggestion.applied || suggestion.id === suggestionId,
+ appliable: false,
+ }));
+ },
+
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
diff --git a/app/assets/javascripts/pages/groups/clusters/index/index.js b/app/assets/javascripts/pages/groups/clusters/index/index.js
index 845a5f7042c..21efc4f6d00 100644
--- a/app/assets/javascripts/pages/groups/clusters/index/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index/index.js
@@ -1,5 +1,7 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
index bf80d8b8193..a63a0dbc6b1 100644
--- a/app/assets/javascripts/pages/groups/index.js
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -1,6 +1,12 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+function initGcpSignupCallout() {
+ const callout = document.querySelector('.gcp-signup-offer');
+
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+}
+
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
@@ -10,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- initDismissableCallout('.gcp-signup-offer');
+ initGcpSignupCallout();
initGkeDropdowns();
}
});
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index 845a5f7042c..21efc4f6d00 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,7 @@
-import initDismissableCallout from '~/dismissable_callout';
+import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 5659e13981a..b0345b4e50d 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,5 @@
-import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
@@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
- initDismissableCallout('.gcp-signup-offer');
+ const callout = document.querySelector('.gcp-signup-offer');
+ if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
+
initGkeDropdowns();
}
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 02a56685a35..f99023ad8e7 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -17,7 +17,7 @@ export default () => {
new MilestoneSelect();
new IssuableTemplateSelectors();
- if (gon.features.issueSuggestions && gon.features.graphql) {
+ if (gon.features.graphql) {
initSuggestions();
}
};
diff --git a/app/assets/javascripts/pages/projects/releases/index/index.js b/app/assets/javascripts/pages/projects/releases/index/index.js
new file mode 100644
index 00000000000..c183fbb9610
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/index/index.js
@@ -0,0 +1,3 @@
+import initReleases from '~/releases';
+
+document.addEventListener('DOMContentLoaded', initReleases);
diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js
index eec2b5ca8e5..e9ecec717d6 100644
--- a/app/assets/javascripts/pages/users/user_overview_block.js
+++ b/app/assets/javascripts/pages/users/user_overview_block.js
@@ -29,18 +29,21 @@ export default class UserOverviewBlock {
render(data) {
const { html, count } = data;
- const contentList = document.querySelector(`${this.container} .overview-content-list`);
+ const containerEl = document.querySelector(this.container);
+ const contentList = containerEl.querySelector('.overview-content-list');
contentList.innerHTML += html;
- const loadingEl = document.querySelector(`${this.container} .loading`);
+ const loadingEl = containerEl.querySelector('.loading');
if (count && count > 0) {
- document.querySelector(`${this.container} .js-view-all`).classList.remove('hide');
+ containerEl.querySelector('.js-view-all').classList.remove('hide');
} else {
- document
- .querySelector(`${this.container} .nothing-here-block`)
- .classList.add('text-left', 'p-0');
+ const nothingHereBlock = containerEl.querySelector('.nothing-here-block');
+
+ if (nothingHereBlock) {
+ nothingHereBlock.classList.add('text-left', 'p-0');
+ }
}
loadingEl.classList.add('hide');
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
new file mode 100644
index 00000000000..1e34e74a152
--- /dev/null
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -0,0 +1,34 @@
+import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
+import Flash from './flash';
+
+export default class PersistentUserCallout {
+ constructor(container) {
+ const { dismissEndpoint, featureId } = container.dataset;
+ this.container = container;
+ this.dismissEndpoint = dismissEndpoint;
+ this.featureId = featureId;
+
+ this.init();
+ }
+
+ init() {
+ const closeButton = this.container.querySelector('.js-close');
+ closeButton.addEventListener('click', event => this.dismiss(event));
+ }
+
+ dismiss(event) {
+ event.preventDefault();
+
+ axios
+ .post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ })
+ .then(() => {
+ this.container.remove();
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ }
+}
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 30a5bbf92ce..7d8863dff29 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -65,7 +65,7 @@ export default {
v-if="pipeline.flags.latest"
v-gl-tooltip
class="js-pipeline-url-latest badge badge-success"
- title="__('Latest pipeline for this branch')"
+ :title="__('Latest pipeline for this branch')"
>
latest
</span>
@@ -100,7 +100,7 @@ export default {
<span
v-if="pipeline.flags.merge_request"
v-gl-tooltip
- title="__('This pipeline is run in a merge request context')"
+ :title="__('This pipeline is run in a merge request context')"
class="js-pipeline-url-mergerequest badge badge-info"
>
merge request
diff --git a/app/assets/javascripts/releases/components/app.vue b/app/assets/javascripts/releases/components/app.vue
new file mode 100644
index 00000000000..0ad5ee2915c
--- /dev/null
+++ b/app/assets/javascripts/releases/components/app.vue
@@ -0,0 +1,82 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import ReleaseBlock from './release_block.vue';
+
+export default {
+ name: 'ReleasesApp',
+ components: {
+ GlLoadingIcon,
+ GlEmptyState,
+ ReleaseBlock,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ documentationLink: {
+ type: String,
+ required: true,
+ },
+ illustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['isLoading', 'releases', 'hasError']),
+ shouldRenderEmptyState() {
+ return !this.releases.length && !this.hasError && !this.isLoading;
+ },
+ shouldRenderSuccessState() {
+ return this.releases.length && !this.isLoading && !this.hasError;
+ },
+ },
+ created() {
+ this.fetchReleases(this.projectId);
+ },
+ methods: {
+ ...mapActions(['fetchReleases']),
+ },
+};
+</script>
+<template>
+ <div class="prepend-top-default">
+ <gl-loading-icon v-if="isLoading" :size="2" class="js-loading prepend-top-20" />
+
+ <gl-empty-state
+ v-else-if="shouldRenderEmptyState"
+ class="js-empty-state"
+ :title="__('Getting started with releases')"
+ :svg-path="illustrationPath"
+ :description="
+ __(
+ 'Releases mark specific points in a project\'s development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API.',
+ )
+ "
+ :primary-button-link="documentationLink"
+ :primary-button-text="__('Open Documentation')"
+ />
+
+ <div v-else-if="shouldRenderSuccessState" class="js-success-state">
+ <release-block
+ v-for="(release, index) in releases"
+ :key="release.tag_name"
+ :release="release"
+ :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
+ />
+ </div>
+ </div>
+</template>
+<style>
+.linked-card::after {
+ width: 1px;
+ content: ' ';
+ border: 1px solid #e5e5e5;
+ height: 17px;
+ top: 100%;
+ position: absolute;
+ left: 32px;
+}
+</style>
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
new file mode 100644
index 00000000000..34b97826cdb
--- /dev/null
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -0,0 +1,129 @@
+<script>
+import _ from 'underscore';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { sprintf } from '../../locale';
+
+export default {
+ name: 'ReleaseBlock',
+ components: {
+ GlLink,
+ Icon,
+ UserAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ release: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ releasedTimeAgo() {
+ return sprintf('released %{time}', {
+ time: this.timeFormated(this.release.created_at),
+ });
+ },
+ userImageAltDescription() {
+ return this.author && this.author.username
+ ? sprintf("%{username}'s avatar", { username: this.author.username })
+ : null;
+ },
+ commit() {
+ return this.release.commit || {};
+ },
+ assets() {
+ return this.release.assets || {};
+ },
+ author() {
+ return this.release.author || {};
+ },
+ hasAuthor() {
+ return _.isEmpty(this.author);
+ },
+ },
+};
+</script>
+<template>
+ <div class="card">
+ <div class="card-body">
+ <h2 class="card-title mt-0">{{ release.name }}</h2>
+
+ <div class="card-subtitle d-flex flex-wrap text-secondary">
+ <div class="append-right-8">
+ <icon name="commit" class="align-middle" />
+ <span v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span>
+ </div>
+
+ <div class="append-right-8">
+ <icon name="tag" class="align-middle" />
+ <span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
+ </div>
+
+ <div class="append-right-4">
+ &bull;
+ <span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">{{
+ releasedTimeAgo
+ }}</span>
+ </div>
+
+ <div v-if="hasAuthor" class="d-flex">
+ by
+ <user-avatar-link
+ class="prepend-left-4"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="userImageAltDescription"
+ :tooltip-text="author.username"
+ />
+ </div>
+ </div>
+
+ <div
+ v-if="assets.links.length || assets.sources.length"
+ Sclass="card-text prepend-top-default"
+ >
+ <b>
+ {{ __('Assets') }}
+ <span class="js-assets-count badge badge-pill">{{ assets.count }}</span>
+ </b>
+
+ <ul v-if="assets.links.length" class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list">
+ <li v-for="link in assets.links" :key="link.name" class="append-bottom-8">
+ <gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.url">
+ <icon name="package" class="align-middle append-right-4 align-text-bottom" />
+ {{ link.name }}
+ </gl-link>
+ </li>
+ </ul>
+
+ <div v-if="assets.sources.length" class="dropdown">
+ <button
+ type="button"
+ class="btn btn-link"
+ data-toggle="dropdown"
+ aria-haspopup="true"
+ aria-expanded="false"
+ >
+ <icon name="doc-code" class="align-top append-right-4" /> {{ __('Source code') }}
+ <icon name="arrow-down" />
+ </button>
+
+ <div class="js-sources-dropdown dropdown-menu">
+ <li v-for="asset in assets.sources" :key="asset.url">
+ <gl-link :href="asset.url">{{ __('Download') }} {{ asset.format }}</gl-link>
+ </li>
+ </div>
+ </div>
+ </div>
+
+ <div class="card-text prepend-top-default"><div v-html="release.description_html"></div></div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/index.js b/app/assets/javascripts/releases/index.js
new file mode 100644
index 00000000000..adbed3cb8e2
--- /dev/null
+++ b/app/assets/javascripts/releases/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+ const element = document.getElementById('js-releases-page');
+
+ return new Vue({
+ el: element,
+ store: createStore(),
+ components: {
+ App,
+ },
+ render(createElement) {
+ return createElement('app', {
+ props: {
+ projectId: element.dataset.projectId,
+ documentationLink: element.dataset.documentationPath,
+ illustrationPath: element.dataset.illustrationPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/releases/store/actions.js b/app/assets/javascripts/releases/store/actions.js
new file mode 100644
index 00000000000..baa2251403e
--- /dev/null
+++ b/app/assets/javascripts/releases/store/actions.js
@@ -0,0 +1,37 @@
+import * as types from './mutation_types';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import api from '~/api';
+
+/**
+ * Commits a mutation to update the state while the main endpoint is being requested.
+ */
+export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
+
+/**
+ * Fetches the main endpoint.
+ * Will dispatch requestNamespace action before starting the request.
+ * Will dispatch receiveNamespaceSuccess if the request is successfull
+ * Will dispatch receiveNamesapceError if the request returns an error
+ *
+ * @param {String} projectId
+ */
+export const fetchReleases = ({ dispatch }, projectId) => {
+ dispatch('requestReleases');
+
+ api
+ .releases(projectId)
+ .then(({ data }) => dispatch('receiveReleasesSuccess', data))
+ .catch(() => dispatch('receiveReleasesError'));
+};
+
+export const receiveReleasesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_RELEASES_SUCCESS, data);
+
+export const receiveReleasesError = ({ commit }) => {
+ commit(types.RECEIVE_RELEASES_ERROR);
+ createFlash(__('An error occured while fetching the releases. Please try again.'));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/releases/store/index.js b/app/assets/javascripts/releases/store/index.js
new file mode 100644
index 00000000000..968b94f0e0d
--- /dev/null
+++ b/app/assets/javascripts/releases/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/releases/store/mutation_types.js b/app/assets/javascripts/releases/store/mutation_types.js
new file mode 100644
index 00000000000..a74bf15c515
--- /dev/null
+++ b/app/assets/javascripts/releases/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const REQUEST_RELEASES = 'REQUEST_RELEASES';
+export const RECEIVE_RELEASES_SUCCESS = 'RECEIVE_RELEASES_SUCCESS';
+export const RECEIVE_RELEASES_ERROR = 'RECEIVE_RELEASES_ERROR';
diff --git a/app/assets/javascripts/releases/store/mutations.js b/app/assets/javascripts/releases/store/mutations.js
new file mode 100644
index 00000000000..b97dc6cb0ab
--- /dev/null
+++ b/app/assets/javascripts/releases/store/mutations.js
@@ -0,0 +1,37 @@
+import * as types from './mutation_types';
+
+export default {
+ /**
+ * Sets isLoading to true while the request is being made.
+ * @param {Object} state
+ */
+ [types.REQUEST_RELEASES](state) {
+ state.isLoading = true;
+ },
+
+ /**
+ * Sets isLoading to false.
+ * Sets hasError to false.
+ * Sets the received data
+ * @param {Object} state
+ * @param {Object} data
+ */
+ [types.RECEIVE_RELEASES_SUCCESS](state, data) {
+ state.hasError = false;
+ state.isLoading = false;
+ state.releases = data;
+ },
+
+ /**
+ * Sets isLoading to false.
+ * Sets hasError to true.
+ * Resets the data
+ * @param {Object} state
+ * @param {Object} data
+ */
+ [types.RECEIVE_RELEASES_ERROR](state) {
+ state.isLoading = false;
+ state.releases = [];
+ state.hasError = true;
+ },
+};
diff --git a/app/assets/javascripts/releases/store/state.js b/app/assets/javascripts/releases/store/state.js
new file mode 100644
index 00000000000..bf25e651c99
--- /dev/null
+++ b/app/assets/javascripts/releases/store/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ isLoading: false,
+ hasError: false,
+ releases: [],
+});
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 225e21ad322..9a0cdc02952 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -79,11 +79,12 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
- ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
- if ($this.attr('data-delete-path')) {
- url = '' + $this.attr('data-delete-path');
+ ajaxType = $this.data('deletePath') ? 'delete' : 'post';
+
+ if ($this.data('deletePath')) {
+ url = '' + $this.data('deletePath');
} else {
- url = '' + $this.data('url');
+ url = '' + $this.data('createPath');
}
$this.tooltip('hide');
@@ -119,14 +120,14 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.removeClass('is-loading')
.enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`))
- .attr('data-delete-path', deletePath)
- .attr('title', $el.data(`${attrPrefix}Text`));
+ .attr('title', $el.data(`${attrPrefix}Text`))
+ .data('deletePath', deletePath);
if ($el.hasClass('has-tooltip')) {
$el.tooltip('_fixTitle');
}
- if ($el.data(`${attrPrefix}Icon`)) {
+ if (typeof $el.data('isCollapsed') !== 'undefined') {
$elText.html($el.data(`${attrPrefix}Icon`));
} else {
$elText.text($el.data(`${attrPrefix}Text`));
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 7874a7b6b6a..349e14670b1 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -81,7 +81,7 @@ export default {
</p>
<ul>
<li>Your repository does not have a corresponding <code>serverless.yml</code> file.</li>
- <li>Your <code>gitlab-ci.yml</code> file is not properly configured.</li>
+ <li>Your <code>.gitlab-ci.yml</code> file is not properly configured.</li>
<li>
The functions listed in the <code>serverless.yml</code> file don't match the namespace
of your cluster.
diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
index 14a89ef9293..3a8631a196f 100644
--- a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
+++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js
@@ -12,9 +12,8 @@ class EmojiMenuInModal extends AwardsHandler {
this.bindEvents();
}
- postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
+ postEmoji($emojiButton, awardUrl, selectedEmoji) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
- callback();
}
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index f20cc6d8cca..7b8b4c5d856 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -71,7 +71,7 @@ export default class SidebarStore {
}
findAssignee(findAssignee) {
- return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
+ return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
removeAssignee(removeAssignee) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index adfbcd18588..0bcccc50eb2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -72,7 +72,7 @@ export default {
Flash('Something went wrong. Please try again.');
}
- eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('MRWidgetRebaseSuccess');
stopPolling();
}
})
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 3c3e3efcc36..b7f12076958 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
@@ -106,6 +106,9 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
+ shouldRenderCollaborationStatus() {
+ return this.mr.allowCollaboration && this.mr.isOpen;
+ },
shouldRenderMergedPipeline() {
return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
},
@@ -155,13 +158,13 @@ export default {
};
return new MRWidgetService(endpoints);
},
- checkStatus(cb) {
+ checkStatus(cb, isRebased) {
return this.service
.checkStatus()
.then(res => res.data)
.then(data => {
this.handleNotification(data);
- this.mr.setData(data);
+ this.mr.setData(data, isRebased);
this.setFaviconHelper();
if (cb) {
@@ -263,6 +266,10 @@ export default {
this.checkStatus(cb);
});
+ eventHub.$on('MRWidgetRebaseSuccess', cb => {
+ this.checkStatus(cb, true);
+ });
+
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', params => {
@@ -311,7 +318,7 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
- <section v-if="mr.allowCollaboration" class="mr-info-list mr-links">
+ <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
{{ s__('mrWidget|Allows commits from members who can merge to the target branch') }}
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index f7f0c1b6cb7..066a3b833d7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -19,7 +19,7 @@ export default function deviseState(data) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
return stateKey.pipelineBlocked;
- } else if (this.hasSHAChanged) {
+ } else if (this.isSHAMismatch) {
return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
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 5c9a7133a6e..c777bcca0fa 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
@@ -11,7 +11,11 @@ export default class MergeRequestStore {
this.setData(data);
}
- setData(data) {
+ setData(data, isRebased) {
+ if (isRebased) {
+ this.sha = data.diff_head_sha;
+ }
+
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
@@ -84,7 +88,7 @@ export default class MergeRequestStore {
this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
- this.hasSHAChanged = this.sha !== data.diff_head_sha;
+ this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
index ddbb14ae812..56bafebf4ce 100644
--- a/app/assets/javascripts/vue_shared/components/callout.vue
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -11,13 +11,14 @@ export default {
},
message: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
};
</script>
<template>
<div :class="`bs-callout bs-callout-${category}`" role="alert" aria-live="assertive">
- {{ message }}
+ {{ message }} <slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
new file mode 100644
index 00000000000..53210cbcc93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
@@ -0,0 +1,3 @@
+<template>
+ <div class="nothing-here-block">{{ __('Empty file') }}</div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
new file mode 100644
index 00000000000..df6fadf10cd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -0,0 +1,69 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlModal } from '@gitlab/ui';
+
+/**
+ * This component keeps the GlModal's visibility in sync with the given vuex module.
+ */
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ modalModule: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState({
+ isVisible(state) {
+ return state[this.modalModule].isVisible;
+ },
+ }),
+ attrs() {
+ const { modalId, modalModule, ...attrs } = this.$attrs;
+
+ return attrs;
+ },
+ },
+ watch: {
+ isVisible(val) {
+ return val ? this.bsShow() : this.bsHide();
+ },
+ },
+ methods: {
+ ...mapActions({
+ syncShow(dispatch) {
+ return dispatch(`${this.modalModule}/show`);
+ },
+ syncHide(dispatch) {
+ return dispatch(`${this.modalModule}/hide`);
+ },
+ }),
+ bsShow() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ bsHide() {
+ // $root.$emit is a workaround because other b-modal approaches don't work yet with gl-modal
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ v-bind="attrs"
+ :modal-id="modalId"
+ v-on="$listeners"
+ @shown="syncShow"
+ @hidden="syncHide"
+ >
+ <slot></slot>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 43def2673eb..2f7ed4a982c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,17 +1,21 @@
<script>
import $ from 'jquery';
+import _ from 'underscore';
import { __ } from '~/locale';
+import { stripHtml } from '~/lib/utils/text_utility';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
+import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
export default {
components: {
markdownHeader,
markdownToolbar,
icon,
+ Suggestions,
},
props: {
markdownPreviewPath: {
@@ -48,12 +52,33 @@ export default {
required: false,
default: true,
},
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ note: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ canSuggest: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
markdownPreview: '',
referencedCommands: '',
referencedUsers: '',
+ hasSuggestion: false,
markdownPreviewLoading: false,
previewMarkdown: false,
};
@@ -63,6 +88,39 @@ export default {
const referencedUsersThreshold = 10;
return this.referencedUsers.length >= referencedUsersThreshold;
},
+ lineContent() {
+ const FIRST_CHAR_REGEX = /^(\+|-)/;
+ const [firstSuggestion] = this.suggestions;
+ if (firstSuggestion) {
+ return firstSuggestion.from_content;
+ }
+
+ if (this.line) {
+ const { rich_text: richText, text } = this.line;
+
+ if (text) {
+ return text.replace(FIRST_CHAR_REGEX, '');
+ }
+
+ return _.unescape(stripHtml(richText).replace(/\n/g, ''));
+ }
+
+ return '';
+ },
+ lineNumber() {
+ let lineNumber;
+ if (this.line) {
+ const { new_line: newLine, old_line: oldLine } = this.line;
+ lineNumber = newLine || oldLine;
+ }
+ return lineNumber;
+ },
+ suggestions() {
+ return this.note.suggestions || [];
+ },
+ lineType() {
+ return this.line ? this.line.type : '';
+ },
},
mounted() {
/*
@@ -122,6 +180,7 @@ export default {
if (data.references) {
this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users;
+ this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
}
this.$nextTick(() => {
@@ -147,6 +206,8 @@ export default {
>
<markdown-header
:preview-markdown="previewMarkdown"
+ :line-content="lineContent"
+ :can-suggest="canSuggest"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
/>
@@ -163,19 +224,39 @@ export default {
/>
</div>
</div>
- <div
- v-show="previewMarkdown"
- ref="markdown-preview"
- class="md-preview js-vue-md-preview md md-preview-holder"
- v-html="markdownPreview"
- ></div>
+ <template v-if="hasSuggestion">
+ <div
+ v-show="previewMarkdown"
+ ref="markdown-preview"
+ class="md-preview js-vue-md-preview md md-preview-holder"
+ >
+ <suggestions
+ v-if="hasSuggestion"
+ :note-html="markdownPreview"
+ :from-line="lineNumber"
+ :from-content="lineContent"
+ :line-type="lineType"
+ :disabled="true"
+ :suggestions="suggestions"
+ :help-page-path="helpPagePath"
+ />
+ </div>
+ </template>
+ <template v-else>
+ <div
+ v-show="previewMarkdown"
+ ref="markdown-preview"
+ class="md-preview js-vue-md-preview md md-preview-holder"
+ v-html="markdownPreview"
+ ></div>
+ </template>
<template v-if="previewMarkdown && !markdownPreviewLoading">
<div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users">
<span>
- <i class="fa fa-exclamation-triangle" aria-hidden="true"> </i> You are about to add
+ <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> You are about to add
<strong>
- <span class="js-referenced-users-count"> {{ referencedUsers.length }} </span>
+ <span class="js-referenced-users-count">{{ referencedUsers.length }}</span>
</strong>
people to the discussion. Proceed with caution.
</span>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 4c4ba537065..bf4d42670ee 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -17,6 +17,16 @@ export default {
type: Boolean,
required: true,
},
+ lineContent: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canSuggest: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
mdTable() {
@@ -27,6 +37,9 @@ export default {
'| cell | cell |',
].join('\n');
},
+ mdSuggestion() {
+ return ['```suggestion', `{text}`, '```'].join('\n');
+ },
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
@@ -119,6 +132,16 @@ export default {
:button-title="__('Add a table')"
icon="table"
/>
+ <toolbar-button
+ v-if="canSuggest"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ class="qa-suggestion-btn"
+ />
<button
v-gl-tooltip
aria-label="Go full screen"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
new file mode 100644
index 00000000000..f98560f7336
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -0,0 +1,74 @@
+<script>
+import SuggestionDiffHeader from './suggestion_diff_header.vue';
+
+export default {
+ components: {
+ SuggestionDiffHeader,
+ },
+ props: {
+ newLines: {
+ type: Array,
+ required: true,
+ },
+ fromContent: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fromLine: {
+ type: Number,
+ required: true,
+ },
+ suggestion: {
+ type: Object,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ applySuggestion(callback) {
+ this.$emit('apply', { suggestionId: this.suggestion.id, callback });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <suggestion-diff-header
+ class="qa-suggestion-diff-header"
+ :can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled"
+ :is-applied="suggestion.applied"
+ :help-page-path="helpPagePath"
+ @apply="applySuggestion"
+ />
+ <table class="mb-3 md-suggestion-diff">
+ <tbody>
+ <!-- Old Line -->
+ <tr class="line_holder old">
+ <td class="diff-line-num old_line qa-old-diff-line-number old">{{ fromLine }}</td>
+ <td class="diff-line-num new_line old"></td>
+ <td class="line_content old">
+ <span>{{ fromContent }}</span>
+ </td>
+ </tr>
+ <!-- New Line(s) -->
+ <tr v-for="(line, key) of newLines" :key="key" 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">{{ line.lineNumber }}</td>
+ <td class="line_content new">
+ <span>{{ line.content }}</span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
new file mode 100644
index 00000000000..563e2f94fcc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -0,0 +1,60 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: { Icon },
+ props: {
+ canApply: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isApplied: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isAppliedSuccessfully: false,
+ isApplying: false,
+ };
+ },
+ methods: {
+ applySuggestion() {
+ if (!this.canApply) return;
+ this.isApplying = true;
+ this.$emit('apply', this.applySuggestionCallback);
+ },
+ applySuggestionCallback() {
+ this.isApplying = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="md-suggestion-header border-bottom-0 mt-2">
+ <div class="qa-suggestion-diff-header font-weight-bold">
+ {{ __('Suggested change') }}
+ <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')">
+ <icon name="question-o" css-classes="link-highlight" />
+ </a>
+ </div>
+ <span v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</span>
+ <button
+ v-if="canApply"
+ type="button"
+ class="btn qa-apply-btn"
+ :disabled="isApplying"
+ @click="applySuggestion"
+ >
+ {{ __('Apply suggestion') }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
new file mode 100644
index 00000000000..7c6dbee3e19
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -0,0 +1,136 @@
+<script>
+import Vue from 'vue';
+import SuggestionDiff from './suggestion_diff.vue';
+import Flash from '~/flash';
+
+export default {
+ components: { SuggestionDiff },
+ props: {
+ fromLine: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ fromContent: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ suggestions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ noteHtml: {
+ type: String,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isRendered: false,
+ };
+ },
+ watch: {
+ suggestions() {
+ this.reset();
+ },
+ noteHtml() {
+ this.reset();
+ },
+ },
+ mounted() {
+ this.renderSuggestions();
+ },
+ methods: {
+ renderSuggestions() {
+ // swaps out suggestion(s) markdown with rich diff components
+ // (while still keeping non-suggestion markdown in place)
+
+ if (!this.noteHtml) return;
+ const { container } = this.$refs;
+ const suggestionElements = container.querySelectorAll('.js-render-suggestion');
+
+ if (this.lineType === 'old') {
+ Flash('Unable to apply suggestions to a deleted line.', 'alert', this.$el);
+ }
+
+ suggestionElements.forEach((suggestionEl, i) => {
+ const suggestionParentEl = suggestionEl.parentElement;
+ const newLines = this.extractNewLines(suggestionParentEl);
+ const diffComponent = this.generateDiff(newLines, i);
+ diffComponent.$mount(suggestionParentEl);
+ });
+
+ this.isRendered = true;
+ },
+ extractNewLines(suggestionEl) {
+ // extracts the suggested lines from the markdown
+ // calculates a line number for each line
+
+ const FIRST_CHAR_REGEX = /^(\+|-)/;
+ const newLines = suggestionEl.querySelectorAll('.line');
+ const fromLine = this.suggestions.length ? this.suggestions[0].from_line : this.fromLine;
+ const lines = [];
+
+ newLines.forEach((line, i) => {
+ const content = `${line.innerText.replace(FIRST_CHAR_REGEX, '')}\n`;
+ const lineNumber = fromLine + i;
+ lines.push({ content, lineNumber });
+ });
+
+ return lines;
+ },
+ generateDiff(newLines, suggestionIndex) {
+ // generates the diff <suggestion-diff /> component
+ // all `suggestion` markdown will be swapped out by this component
+
+ const { suggestions, disabled, helpPagePath } = this;
+ const suggestion =
+ suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {};
+ const fromContent = suggestion.from_content || this.fromContent;
+ const fromLine = suggestion.from_line || this.fromLine;
+ const SuggestionDiffComponent = Vue.extend(SuggestionDiff);
+ const suggestionDiff = new SuggestionDiffComponent({
+ propsData: { newLines, fromLine, fromContent, disabled, suggestion, helpPagePath },
+ });
+
+ suggestionDiff.$on('apply', ({ suggestionId, callback }) => {
+ this.$emit('apply', { suggestionId, callback, flashContainer: this.$el });
+ });
+
+ return suggestionDiff;
+ },
+ reset() {
+ // resets the container HTML (replaces it with the updated noteHTML)
+ // calls `renderSuggestions` once the updated noteHTML is added to the DOM
+
+ this.$refs.container.innerHTML = this.noteHtml;
+ this.isRendered = false;
+ this.renderSuggestions();
+ this.$nextTick(() => this.renderSuggestions());
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="flash-container mt-3"></div>
+ <div v-show="isRendered" ref="container" v-html="noteHtml"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index a6d2cecdf7e..4572caa907b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -37,6 +37,16 @@ export default {
required: false,
default: false,
},
+ tagContent: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ cursorOffset: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
};
</script>
@@ -45,8 +55,10 @@ export default {
<button
v-gl-tooltip
:data-md-tag="tag"
+ :data-md-cursor-offset="cursorOffset"
:data-md-select="tagSelect"
:data-md-block="tagBlock"
+ :data-md-tag-content="tagContent"
:data-md-prepend="prepend"
:title="buttonTitle"
:aria-label="buttonTitle"
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 e833a8e0483..95f4395ac13 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -67,6 +67,7 @@ export default {
// In both cases we should render the defaultAvatarUrl
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}`;
return baseSrc;
},
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 7fbadcc0111..d24fe1b547e 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
@@ -1,6 +1,5 @@
<script>
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
@@ -28,19 +27,6 @@ export default {
},
},
computed: {
- jobLine() {
- if (this.user.bio && this.user.organization) {
- return sprintf(__('%{bio} at %{organization}'), {
- bio: this.user.bio,
- organization: this.user.organization,
- });
- } else if (this.user.bio) {
- return this.user.bio;
- } else if (this.user.organization) {
- return this.user.organization;
- }
- return null;
- },
statusHtml() {
if (this.user.status.emoji && this.user.status.message) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`;
@@ -82,7 +68,8 @@ export default {
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</div>
<div class="text-secondary">
- {{ jobLine }}
+ <div v-if="user.bio" class="js-bio">{{ user.bio }}</div>
+ <div v-if="user.organization" class="js-organization">{{ user.organization }}</div>
<gl-skeleton-loading
v-if="jobInfoIsLoading"
:lines="1"
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
new file mode 100644
index 00000000000..552237e05c5
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
@@ -0,0 +1,17 @@
+import * as types from './mutation_types';
+
+export const open = ({ commit }, data) => {
+ commit(types.OPEN, data);
+};
+
+export const close = ({ commit }) => {
+ commit(types.CLOSE);
+};
+
+export const show = ({ commit }) => {
+ commit(types.SHOW);
+};
+
+export const hide = ({ commit }) => {
+ commit(types.HIDE);
+};
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/index.js b/app/assets/javascripts/vuex_shared/modules/modal/index.js
new file mode 100644
index 00000000000..c349d875c24
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/index.js
@@ -0,0 +1,10 @@
+import state from './state';
+import mutations from './mutations';
+import * as actions from './actions';
+
+export default () => ({
+ namespaced: true,
+ state: state(),
+ mutations,
+ actions,
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js
new file mode 100644
index 00000000000..f8259736009
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/mutation_types.js
@@ -0,0 +1,4 @@
+export const HIDE = 'HIDE';
+export const SHOW = 'SHOW';
+export const OPEN = 'OPEN';
+export const CLOSE = 'CLOSE';
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/mutations.js b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js
new file mode 100644
index 00000000000..9e96ae8b5a9
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/mutations.js
@@ -0,0 +1,18 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SHOW](state) {
+ state.isVisible = true;
+ },
+ [types.HIDE](state) {
+ state.isVisible = false;
+ },
+ [types.OPEN](state, data) {
+ state.data = data;
+ state.isVisible = true;
+ },
+ [types.CLOSE](state) {
+ state.data = null;
+ state.isVisible = false;
+ },
+};
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/state.js b/app/assets/javascripts/vuex_shared/modules/modal/state.js
new file mode 100644
index 00000000000..5d0955aa9b0
--- /dev/null
+++ b/app/assets/javascripts/vuex_shared/modules/modal/state.js
@@ -0,0 +1,4 @@
+export default () => ({
+ isVisible: false,
+ data: null,
+});
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 985fac11c87..bdf20866197 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -47,6 +47,7 @@
@import "highlight/solarized_dark";
@import "highlight/solarized_light";
@import "highlight/white";
+@import "highlight/none";
/*
* Styles for JS behaviors.
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index f0671e36130..587127bb059 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -70,6 +70,17 @@ h6,
margin-bottom: 10px;
}
+/* Our adjustments to hx & .hx above add unnecessary margins to modal-title
+ and page-title in modals, so we set them to 0 in order to have properly
+ formatted modal headers. */
+.modal-header {
+ .modal-title,
+ .page-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+}
+
h5,
.h5 {
font-size: $gl-font-size;
@@ -134,7 +145,8 @@ table {
pointer-events: none;
}
-.popover {
+.popover,
+.popover-header {
font-size: 14px;
}
@@ -142,7 +154,9 @@ table {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
- .d#{$infix}-table-header-group { display: table-header-group !important; }
+ .d#{$infix}-table-header-group {
+ display: table-header-group !important;
+ }
}
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 834e7ffce81..62d471bc30c 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -31,7 +31,6 @@
@import 'framework/logo';
@import 'framework/markdown_area';
@import 'framework/media_object';
-@import 'framework/mobile';
@import 'framework/modal';
@import 'framework/pagination';
@import 'framework/panels';
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 549a8730301..43d4044033f 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -260,3 +260,25 @@ $skeleton-line-widths: (
.slide-down-leave-to {
transform: translateY(-30%);
}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg);}
+ 100% { transform: rotate(360deg);}
+}
+
+/** COMMON ANIMATION CLASSES **/
+.transform-origin-center { @include webkit-prefix(transform-origin, 50% 50%); }
+.animate-n-spin { @include webkit-prefix(animation-name, spin); }
+.animate-c-infinite { @include webkit-prefix(animation-iteration-count, infinite); }
+.animate-t-linear { @include webkit-prefix(animation-timing-function, linear); }
+.animate-d-1 { @include webkit-prefix(animation-duration, 1s); }
+.animate-d-2 { @include webkit-prefix(animation-duration, 2s); }
+
+/** COMPOSITE ANIMATION CLASSES **/
+.gl-spinner {
+ @include webkit-prefix(animation-name, spin);
+ @include webkit-prefix(animation-iteration-count, infinite);
+ @include webkit-prefix(animation-timing-function, linear);
+ @include webkit-prefix(animation-duration, 1s);
+ transform-origin: 50% 50%;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b47b1cb76dc..afcb230797a 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -534,8 +534,9 @@
.dropdown-title {
position: relative;
- padding: 2px 25px 10px;
- margin: 0 10px 10px;
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
+ padding-bottom: #{2 * $dropdown-item-padding-y};
+ margin-bottom: $dropdown-item-padding-y;
font-weight: $gl-font-weight-bold;
line-height: 1;
text-align: center;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 3ac7b6b704b..037a5adfb7e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -24,7 +24,7 @@
}
}
- &:not(.use-csslab) table {
+ table {
@extend .table;
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index afd888af672..4da2243981e 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -256,7 +256,12 @@ label {
}
}
+.input-md {
+ max-width: $input-md-width;
+ width: 100%;
+}
+
.input-lg {
- max-width: 320px;
+ max-width: $input-lg-width;
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 7d283dcfb71..5574873fa22 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -597,3 +597,11 @@
@include emoji-menu-toggle-button;
}
}
+
+.nav-links > li > a {
+ .badge.badge-pill {
+ @include media-breakpoint-down(xs) { display: none; }
+ }
+
+ @include media-breakpoint-down(xs) { margin-right: 3px; }
+}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 73533571a2f..946f575ac13 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -42,7 +42,6 @@
padding: 10px;
text-align: right;
float: left;
- line-height: 1;
a {
font-family: $monospace-font;
@@ -69,3 +68,9 @@
}
}
}
+
+// Vertically aligns <table> line numbers (eg. blame view)
+// see https://gitlab.com/gitlab-org/gitlab-ce/issues/54048
+td.line-numbers {
+ line-height: 1;
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index a66604e56ff..e51f230a680 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -45,9 +45,4 @@
&.status-box-upcoming {
background: $gl-text-color-secondary;
}
-
- &.status-box-milestone {
- color: $gl-text-color;
- background: $gray-darker;
- }
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 9218df9b40f..97cb9d90ff0 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -40,6 +40,14 @@ body {
.content {
margin: 0;
+
+ @include media-breakpoint-down(xs) { margin-top: 20px; }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .container .title {
+ padding-left: 15px !important;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 2b110e23fb8..ce46d760d7b 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -173,7 +173,7 @@
svg {
width: 14px;
height: 14px;
- margin-top: 3px;
+ vertical-align: middle;
fill: $gl-text-color-secondary;
}
@@ -277,6 +277,27 @@
}
}
+.md-suggestion-diff {
+ display: table !important;
+ border: 1px solid $border-color !important;
+}
+
+.md-suggestion-header {
+ height: $suggestion-header-height;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ padding: $gl-padding;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+
+ svg {
+ vertical-align: middle;
+ margin-bottom: 3px;
+ }
+}
+
@include media-breakpoint-down(xs) {
.atwho-view-ul {
width: 350px;
@@ -286,4 +307,8 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+ .referenced-users {
+ margin-right: 0;
+ }
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
deleted file mode 100644
index 3bb046d0e51..00000000000
--- a/app/assets/stylesheets/framework/mobile.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-/** Common mobile (screen XS, SM) styles **/
-@include media-breakpoint-down(xs) {
- .container .content {
- margin-top: 20px;
- }
-
- .nav-links > li > a {
- padding: 10px;
- font-size: 12px;
- margin-right: 3px;
-
- .badge.badge-pill {
- display: none;
- }
- }
-
- .referenced-users {
- margin-right: 0;
- }
-
- .issues-details-filters:not(.filtered-search-block),
- .dash-projects-filters,
- .check-all-holder {
- display: none;
- }
-
- .rss-btn {
- display: none;
- }
-
- .project-home-links {
- display: none;
- }
-
- .project-home-panel {
- padding-left: 0 !important;
-
- .project-repo-buttons,
- .git-clone-holder {
- display: none;
- }
- }
-
- .group-buttons {
- display: none;
- }
-
- .container .title {
- padding-left: 15px !important;
- }
-
- .nav-links,
- .nav-links {
- li a {
- font-size: 14px;
- padding: 19px 10px;
- }
- }
-
- .activity-filter-block {
- display: none;
- }
-
- .projects-search-form {
- .btn {
- display: none;
- }
- }
-}
-
-@include media-breakpoint-down(sm) {
- .issues-filters {
- .milestone-filter {
- display: none;
- }
- }
-
- .page-title {
- .note-created-ago,
- .new-issue-link {
- display: none;
- }
- }
-
- aside:not(.right-sidebar) {
- display: none;
- }
-}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 7e30747963a..46d40ea7aa5 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -25,14 +25,10 @@
&.w-100 {
// after upgrading to Bootstrap 4.2 we can use $modal-header-padding-x here
// https://github.com/twbs/bootstrap/pull/26976
- margin-right: -2rem;
- padding-right: 2rem;
+ margin-right: -28px;
+ padding-right: 28px;
}
}
-
- .page-title {
- margin-top: 0;
- }
}
.modal-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 7f0edd88dfb..a68f1e4e570 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,6 +1,11 @@
/** Select2 selectbox style override **/
.select2-container {
width: 100% !important;
+
+ &.input-md,
+ &.input-lg {
+ display: block;
+ }
}
.select2-container,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a92481b3ebb..d92d81b2cb5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -32,6 +32,15 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
$gray-darker: #eee;
$gray-darkest: #c4c4c4;
+$black: #000;
+$black-transparent: rgba(0, 0, 0, 0.3);
+$almost-black: #242424;
+
+$t-gray-a-02: rgba($black, 0.02);
+$t-gray-a-04: rgba($black, 0.04);
+$t-gray-a-06: rgba($black, 0.06);
+$t-gray-a-08: rgba($black, 0.08);
+
$gl-gray-100: #dddddd;
$gl-gray-200: #cccccc;
$gl-gray-350: #aaaaaa;
@@ -170,11 +179,6 @@ $theme-light-red-500: #c24b38;
$theme-light-red-600: #b03927;
$theme-light-red-700: #a62e21;
-$black: #000;
-$black-transparent: rgba(0, 0, 0, 0.3);
-$shadow-color: rgba($black, 0.1);
-$almost-black: #242424;
-
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
@@ -187,6 +191,7 @@ $border-gray-dark: darken($white-normal, $darken-border-factor);
* UI elements
*/
$border-color: #e5e5e5;
+$shadow-color: $t-gray-a-08;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -198,7 +203,6 @@ $well-light-text-color: #5b6169;
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
-$gl-font-size-medium: 1.43rem;
$gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -252,6 +256,7 @@ $browserScrollbarSize: 10px;
* Misc
*/
$header-height: 40px;
+$suggestion-header-height: 46px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
@@ -508,6 +513,8 @@ $gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
$input-short-width: 200px;
$input-short-md-width: 280px;
+$input-md-width: 240px;
+$input-lg-width: 320px;
/*
* Help
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 5ca76bb6c5a..069f45bff49 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -28,3 +28,9 @@ $popover-border-width: 1px;
$popover-border-color: $border-color;
$popover-box-shadow: 0 $border-radius-small $border-radius-default 0 $shadow-color;
$popover-arrow-outer-color: $shadow-color;
+$h1-font-size: 14px * 2.5;
+$h2-font-size: 14px * 2;
+$h3-font-size: 14px * 1.75;
+$h4-font-size: 14px * 1.5;
+$h5-font-size: 14px * 1.25;
+$h6-font-size: 14px;
diff --git a/app/assets/stylesheets/highlight/none.scss b/app/assets/stylesheets/highlight/none.scss
new file mode 100644
index 00000000000..7d692a87e33
--- /dev/null
+++ b/app/assets/stylesheets/highlight/none.scss
@@ -0,0 +1,242 @@
+/*
+* None Syntax Colors
+*/
+
+
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $white-normal;
+}
+
+.code.none {
+ // Line numbers
+ .line-numbers,
+ .diff-line-num {
+ background-color: $gray-light;
+ }
+
+ .diff-line-num,
+ .diff-line-num a {
+ color: $black-transparent;
+ }
+
+ // Code itself
+ pre.code,
+ .diff-line-num {
+ border-color: $white-normal;
+ }
+
+ &,
+ pre.code,
+ .line_holder .line_content {
+ background-color: $white-light;
+ color: $gl-text-color;
+ }
+
+// Diff line
+
+ $none-over-bg: #ded7fc;
+ $none-expanded-border: #e0e0e0;
+ $none-expanded-bg: #f7f7f7;
+
+ .line_holder {
+
+ &.match .line_content,
+ .new-nonewline.line_content,
+ .old-nonewline.line_content {
+ @include matchLine;
+ }
+
+ .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%);
+ }
+ }
+
+ &.is-over,
+ &.hll:not(.empty-cell).is-over {
+ background-color: $none-over-bg;
+ border-color: darken($none-over-bg, 5%);
+
+ a {
+ color: darken($none-over-bg, 15%);
+ }
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-number-select;
+ border-color: $line-select-yellow-dark;
+ }
+ }
+
+ &:not(.diff-expanded) + .diff-expanded,
+ &.diff-expanded + .line_holder:not(.diff-expanded) {
+ > .diff-line-num,
+ > .line_content {
+ border-top: 1px solid $none-expanded-border;
+ }
+ }
+
+ &.diff-expanded {
+ > .diff-line-num,
+ > .line_content {
+ background: $none-expanded-bg;
+ border-color: $none-expanded-bg;
+ }
+ }
+
+ .line_content {
+ &.old {
+ background-color: $line-removed;
+
+ &::before {
+ color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-removed-dark;
+ }
+ }
+
+ &.new {
+ background-color: $line-added;
+
+ &::before {
+ color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
+ }
+
+ span.idiff {
+ background-color: $line-added-dark;
+ }
+ }
+
+ &.match {
+ @include matchLine;
+ }
+
+ &.hll:not(.empty-cell) {
+ background-color: $line-select-yellow;
+ }
+ }
+ }
+
+ // highlight line via anchor
+ pre .hll {
+ background-color: $white-normal;
+ }
+
+ // Search result highlight
+ span.highlight_word {
+ background-color: $white-normal;
+ }
+
+ // Links to URLs, emails, or dependencies
+ .line a {
+ color: $gl-text-color;
+ text-decoration: underline;
+ }
+
+ .hll { background-color: $white-light; }
+
+ .gd {
+ color: $gl-text-color;
+ background-color: $white-light;
+
+ .x {
+ color: $gl-text-color;
+ background-color: $white-light;
+ }
+ }
+
+ .gi {
+ color: $gl-text-color;
+ background-color: $white-light;
+
+ .x {
+ color: $gl-text-color;
+ background-color: $white-light;
+ }
+ }
+
+ .c { color: $gl-text-color; } /* Comment */
+ .err { color: $gl-text-color; } /* Error */
+ .g { color: $gl-text-color; } /* Generic */
+ .k { color: $gl-text-color; } /* Keyword */
+ .l { color: $gl-text-color; } /* Literal */
+ .n { color: $gl-text-color; } /* Name */
+ .o { color: $gl-text-color; } /* Operator */
+ .x { color: $gl-text-color; } /* Other */
+ .p { color: $gl-text-color; } /* Punctuation */
+ .cm { color: $gl-text-color; } /* Comment.Multiline */
+ .cp { color: $gl-text-color; } /* Comment.Preproc */
+ .c1 { color: $gl-text-color; } /* Comment.Single */
+ .cs { color: $gl-text-color; } /* Comment.Special */
+ .ge { color: $gl-text-color; } /* Generic.Emph */
+ .gr { color: $gl-text-color; } /* Generic.Error */
+ .gh { color: $gl-text-color; } /* Generic.Heading */
+ .go { color: $gl-text-color; } /* Generic.Output */
+ .gp { color: $gl-text-color; } /* Generic.Prompt */
+ .gs { color: $gl-text-color; } /* Generic.Strong */
+ .gu { color: $gl-text-color; } /* Generic.Subheading */
+ .gt { color: $gl-text-color; } /* Generic.Traceback */
+ .kc { color: $gl-text-color; } /* Keyword.Constant */
+ .kd { color: $gl-text-color; } /* Keyword.Declaration */
+ .kn { color: $gl-text-color; } /* Keyword.Namespace */
+ .kp { color: $gl-text-color; } /* Keyword.Pseudo */
+ .kr { color: $gl-text-color; } /* Keyword.Reserved */
+ .kt { color: $gl-text-color; } /* Keyword.Type */
+ .ld { color: $gl-text-color; } /* Literal.Date */
+ .m { color: $gl-text-color; } /* Literal.Number */
+ .s { color: $gl-text-color; } /* Literal.String */
+ .na { color: $gl-text-color; } /* Name.Attribute */
+ .nb { color: $gl-text-color; } /* Name.Builtin */
+ .nc { color: $gl-text-color; } /* Name.Class */
+ .no { color: $gl-text-color; } /* Name.Constant */
+ .nd { color: $gl-text-color; } /* Name.Decorator */
+ .ni { color: $gl-text-color; } /* Name.Entity */
+ .ne { color: $gl-text-color; } /* Name.Exception */
+ .nf { color: $gl-text-color; } /* Name.Function */
+ .nl { color: $gl-text-color; } /* Name.Label */
+ .nn { color: $gl-text-color; } /* Name.Namespace */
+ .nx { color: $gl-text-color; } /* Name.Other */
+ .py { color: $gl-text-color; } /* Name.Property */
+ .nt { color: $gl-text-color; } /* Name.Tag */
+ .nv { color: $gl-text-color; } /* Name.Variable */
+ .ow { color: $gl-text-color; } /* Operator.Word */
+ .w { color: $gl-text-color; } /* Text.Whitespace */
+ .mf { color: $gl-text-color; } /* Literal.Number.Float */
+ .mh { color: $gl-text-color; } /* Literal.Number.Hex */
+ .mi { color: $gl-text-color; } /* Literal.Number.Integer */
+ .mo { color: $gl-text-color; } /* Literal.Number.Oct */
+ .sb { color: $gl-text-color; } /* Literal.String.Backtick */
+ .sc { color: $gl-text-color; } /* Literal.String.Char */
+ .sd { color: $gl-text-color; } /* Literal.String.Doc */
+ .s2 { color: $gl-text-color; } /* Literal.String.Double */
+ .se { color: $gl-text-color; } /* Literal.String.Escape */
+ .sh { color: $gl-text-color; } /* Literal.String.Heredoc */
+ .si { color: $gl-text-color; } /* Literal.String.Interpol */
+ .sx { color: $gl-text-color; } /* Literal.String.Other */
+ .sr { color: $gl-text-color; } /* Literal.String.Regex */
+ .s1 { color: $gl-text-color; } /* Literal.String.Single */
+ .ss { color: $gl-text-color; } /* Literal.String.Symbol */
+ .bp { color: $gl-text-color; } /* Name.Builtin.Pseudo */
+ .vc { color: $gl-text-color; } /* Name.Variable.Class */
+ .vg { color: $gl-text-color; } /* Name.Variable.Global */
+ .vi { color: $gl-text-color; } /* Name.Variable.Instance */
+ .il { color: $gl-text-color; } /* Literal.Number.Integer.Long */
+
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 57918eafd6f..09235661cea 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -135,6 +135,7 @@
.build-loader-animation {
@include build-loader-animation;
float: left;
+ padding-left: $gl-padding-8;
}
}
@@ -232,6 +233,11 @@
@extend .d-flex;
justify-content: space-between;
align-items: center;
+
+ .trigger-variables-btn {
+ margin-top: -5px;
+ margin-bottom: -5px;
+ }
}
.trigger-build-variables {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index f46ff360496..5a988b184b6 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -128,6 +128,10 @@
width: 100%;
}
}
+
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
+ clear: both;
+ }
}
.editor-ref {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 5b5f486ea63..a1069aa9783 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -60,6 +60,10 @@
padding: 0;
margin-bottom: $gl-padding;
border-bottom: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ min-width: 0;
+ width: 100%;
}
.btn-edit {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index d26659701e1..e0f7d075fc7 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -93,8 +93,28 @@ $colors: (
solarized-dark-line-origin-chosen : rgba(#2878c9, .35),
solarized-dark-button-origin-chosen : #0082cc,
- solarized-dark-header-not-chosen : rgba(#839496, .25),
- solarized-dark-line-not-chosen : rgba(#839496, .15)
+ solarized_dark_header_not_chosen : rgba(#839496, .25),
+ solarized_dark_line_not_chosen : rgba(#839496, .15),
+
+ none_header_head_neutral : $gray-normal,
+ none_line_head_neutral : $gray-normal,
+ none_button_head_neutral : $gray-normal,
+
+ none_header_head_chosen : $gray-darker,
+ none_line_head_chosen : $gray-darker,
+ none_button_head_chosen : $gray-darker,
+
+ none_header_origin_neutral : $gray-normal,
+ none_line_origin_neutral : $gray-normal,
+ none_button_origin_neutral : $gray-normal,
+
+ none_header_origin_chosen : $gray-darker,
+ none_line_origin_chosen : $gray-darker,
+ none_button_origin_chosen : $gray-darker,
+
+ none_header_not_chosen : $gray-light,
+ none_line_not_chosen : $gray-light
+
);
// scss-lint:enable ColorVariable
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 1e92582d6d9..94bf32945fc 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -1,3 +1,5 @@
+$status-box-line-height: 26px;
+
.issues-sortable-list .str-truncated {
max-width: 90%;
}
@@ -38,6 +40,7 @@
font-size: $tooltip-font-size;
margin-top: 0;
margin-right: $gl-padding-4;
+ line-height: $status-box-line-height;
@include media-breakpoint-down(xs) {
line-height: unset;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2adfa0d312e..a5b1eff3e1d 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -152,6 +152,16 @@ $note-form-margin-left: 72px;
display: block;
position: relative;
+ .timeline-discussion-body {
+ margin-top: -8px;
+ overflow-x: auto;
+ overflow-y: hidden;
+
+ .discussion-resolved-text {
+ margin-bottom: 8px;
+ }
+ }
+
.diff-content {
overflow: visible;
padding: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index fdd17af35fb..7a47e0a2836 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -978,7 +978,6 @@ button.mini-pipeline-graph-dropdown-toggle {
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
- z-index: 200;
&::before,
&::after {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index a4831b64344..b813eb16dad 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -456,4 +456,15 @@ table.u2f-registrations {
}
}
}
+
+ @include media-breakpoint-down(sm) {
+ .input-md,
+ .input-lg {
+ max-width: 100%;
+ }
+ }
+}
+
+.help-block {
+ color: $gl-text-color-secondary;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0ce0db038a7..004c49dd226 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -973,7 +973,7 @@ pre.light-well {
padding: $gl-padding 0;
@include media-breakpoint-up(lg) {
- padding: $gl-padding-24 0;
+ padding: $gl-padding 0;
}
&.no-description {
@@ -990,7 +990,7 @@ pre.light-well {
}
h2 {
- font-size: $gl-font-size-medium;
+ font-size: $gl-font-size-large;
font-weight: $gl-font-weight-bold;
margin-bottom: 0;
@@ -1049,7 +1049,7 @@ pre.light-well {
}
.controls {
- margin-top: $gl-padding;
+ margin-top: $gl-padding-8;
@include media-breakpoint-down(md) {
margin-top: 0;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8f683ca06ad..8f267eccc8a 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -77,7 +77,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def reset_health_check_token
@application_setting.reset_health_check_access_token!
flash[:notice] = 'New health check access token has been generated!'
- redirect_to :back
+ redirect_back_or_default
end
def clear_repository_check_states
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index 25cc241e5b0..7cd80e8b5e1 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -2,6 +2,12 @@
class Admin::HealthCheckController < Admin::ApplicationController
def show
- @errors = HealthCheck::Utils.process_checks(['standard'])
+ @errors = HealthCheck::Utils.process_checks(checks)
+ end
+
+ private
+
+ def checks
+ ['standard']
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7c8c1392c1c..a8fc848c879 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,9 +12,6 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
include SessionlessAuthentication
- # this can be removed after switching to rails 5
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/51908
- include InvalidUTF8ErrorHandler unless Gitlab.rails5?
before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
@@ -79,7 +76,7 @@ class ApplicationController < ActionController::Base
end
def redirect_back_or_default(default: root_path, options: {})
- redirect_to request.referer.present? ? :back : default, options
+ redirect_back(fallback_location: default, **options)
end
def not_found
@@ -157,7 +154,7 @@ class ApplicationController < ActionController::Base
def log_exception(exception)
Gitlab::Sentry.track_acceptable_exception(exception)
- backtrace_cleaner = Gitlab.rails5? ? request.env["action_dispatch.backtrace_cleaner"] : env
+ backtrace_cleaner = request.env["action_dispatch.backtrace_cleaner"]
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
@@ -406,7 +403,7 @@ class ApplicationController < ActionController::Base
end
def manifest_import_enabled?
- Group.supports_nested_groups? && Gitlab::CurrentSettings.import_sources.include?('manifest')
+ Group.supports_nested_objects? && Gitlab::CurrentSettings.import_sources.include?('manifest')
end
# U2F (universal 2nd factor) devices need a unique identifier for the application
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 9aa8b758539..b9717b97640 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -18,8 +18,20 @@ class Clusters::ClustersController < Clusters::BaseController
STATUS_POLLING_INTERVAL = 10_000
def index
- clusters = ClustersFinder.new(clusterable, current_user, :all).execute
- @clusters = clusters.page(params[:page]).per(20)
+ finder = ClusterAncestorsFinder.new(clusterable.subject, current_user)
+ clusters = finder.execute
+
+ # Note: We are paginating through an array here but this should OK as:
+ #
+ # In CE, we can have a maximum group nesting depth of 21, so including
+ # project cluster, we can have max 22 clusters for a group hierachy.
+ # In EE (Premium) we can have any number, as multiple clusters are
+ # supported, but the number of clusters are fairly low currently.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/55260 also.
+ @clusters = Kaminari.paginate_array(clusters).page(params[:page]).per(20)
+
+ @has_ancestor_clusters = finder.has_ancestor_clusters?
end
def new
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 4f56346832c..e9a7d6a3152 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -32,14 +32,14 @@ module GroupTree
def filtered_groups_with_ancestors(groups)
filtered_groups = groups.search(params[:filter]).page(params[:page])
- if Group.supports_nested_groups?
+ if Group.supports_nested_objects?
# We find the ancestors by ID of the search results here.
# Otherwise the ancestors would also have filters applied,
# which would cause them not to be preloaded.
#
# Pagination needs to be applied before loading the ancestors to
# make sure ancestors are not cut off by pagination.
- Gitlab::GroupHierarchy.new(Group.where(id: filtered_groups.select(:id)))
+ Gitlab::ObjectHierarchy.new(Group.where(id: filtered_groups.select(:id)))
.base_and_ancestors
else
filtered_groups
diff --git a/app/controllers/concerns/invalid_utf8_error_handler.rb b/app/controllers/concerns/invalid_utf8_error_handler.rb
deleted file mode 100644
index 44c6d6b0da0..00000000000
--- a/app/controllers/concerns/invalid_utf8_error_handler.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module InvalidUTF8ErrorHandler
- extend ActiveSupport::Concern
-
- included do
- rescue_from ArgumentError, with: :handle_invalid_utf8
- end
-
- private
-
- def handle_invalid_utf8(error)
- if error.message == "invalid byte sequence in UTF-8"
- render_412
- else
- raise(error)
- end
- end
-
- def render_412
- respond_to do |format|
- format.html { render "errors/precondition_failed", layout: "errors", status: 412 }
- format.js { render json: { error: 'Invalid UTF-8' }, status: :precondition_failed, content_type: 'application/json' }
- format.any { head :precondition_failed }
- end
- end
-end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index ad9cc0925b7..3d64ae8b775 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -5,7 +5,6 @@ module IssuableActions
include Gitlab::Utils::StrongMemoize
included do
- before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
@@ -25,7 +24,10 @@ module IssuableActions
def show
respond_to do |format|
- format.html
+ format.html do
+ @issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
@@ -168,10 +170,6 @@ module IssuableActions
end
end
- def labels
- @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 9576eb14fdd..5572c3cee2d 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -94,6 +94,7 @@ module LfsRequest
def lfs_upload_access?
return false unless project.lfs_enabled?
return false unless has_authentication_ability?(:push_code)
+ return false if limit_exceeded?
lfs_deploy_token? || can?(user, :push_code, project)
end
@@ -121,4 +122,9 @@ module LfsRequest
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
+
+ # Overriden in EE
+ def limit_exceeded?
+ false
+ end
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index c61b9fabe9e..4b0f0b8255c 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -12,7 +12,7 @@ module PreviewMarkdown
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
- when 'projects' then { issuable_state_filter_enabled: true }
+ when 'projects' then projects_filter_params
else {}
end
@@ -22,9 +22,17 @@ module PreviewMarkdown
body: view_context.markdown(result[:text], markdown_params),
references: {
users: result[:users],
+ suggestions: result[:suggestions],
commands: view_context.markdown(result[:commands])
}
}
end
+
+ def projects_filter_params
+ {
+ issuable_state_filter_enabled: true,
+ suggestions_filter_enabled: params[:preview_suggestions].present?
+ }
+ end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 8bd93a349ef..c6ae4fe15bf 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -70,7 +70,7 @@ module ServiceParams
def service_params
dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
- service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
+ service_params = params.permit(:id, service: allowed_service_params + dynamic_params)
if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param|
@@ -80,4 +80,8 @@ module ServiceParams
service_params
end
+
+ def allowed_service_params
+ ALLOWED_PARAMS_CE
+ end
end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 6ea4758ec32..3ef03bc9622 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -43,6 +43,6 @@ class GraphqlController < ApplicationController
end
def check_graphql_feature_flag!
- render_404 unless Feature.enabled?(:graphql)
+ render_404 unless Gitlab::Graphql.enabled?
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index b42116b0f36..868deea3f01 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -43,14 +43,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
- if @milestone.legacy_group_milestone?
- update_params = milestone_params.select { |key| key == "state_event" }
- milestones = @milestone.milestones
- else
- update_params = milestone_params
- milestones = [@milestone]
- end
-
+ milestones, update_params = get_milestones_for_update
milestones.each do |milestone|
Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
end
@@ -71,6 +64,14 @@ class Groups::MilestonesController < Groups::ApplicationController
private
+ def get_milestones_for_update
+ if @milestone.legacy_group_milestone?
+ [@milestone.milestones, legacy_milestone_params]
+ else
+ [[@milestone], milestone_params]
+ end
+ end
+
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestone, group)
end
@@ -79,6 +80,10 @@ class Groups::MilestonesController < Groups::ApplicationController
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
+ def legacy_milestone_params
+ params.require(:milestone).permit(:state_event)
+ end
+
def milestone_path
if @milestone.legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index c1dcc463de7..f476f428fdb 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -4,7 +4,7 @@ module Groups
module Settings
class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
- before_action :authorize_admin_pipeline!
+ before_action :authorize_admin_group!
def show
define_ci_variables
@@ -26,8 +26,8 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
- def authorize_admin_pipeline!
- return render_404 unless can?(current_user, :admin_pipeline, group)
+ def authorize_admin_group!
+ return render_404 unless can?(current_user, :admin_group, group)
end
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index dcee8eb7e6e..055d900eece 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -40,7 +40,6 @@ class Profiles::KeysController < Profiles::ApplicationController
begin
user = UserFinder.new(params[:username]).find_by_username
if user.present?
- headers['Content-Disposition'] = 'attachment'
render plain: user.all_ssh_keys.join("\n")
else
return render_404
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 60fabd15333..ff286c0ccf0 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -260,7 +260,7 @@ class Projects::BlobController < Projects::ApplicationController
extension: blob.extension,
size: blob.raw_size,
mime_type: blob.mime_type,
- binary: blob.raw_binary?,
+ binary: blob.binary?,
simple_viewer: blob.simple_viewer&.class&.partial_name,
rich_viewer: blob.rich_viewer&.class&.partial_name,
show_viewer_switcher: !!blob.show_viewer_switcher?,
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 0a593bd35b6..6824a07dc76 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -24,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKeys::CreateService.new(current_user, create_params).execute
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute(project: @project)
unless @key.valid?
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index c0aa39d87c6..30e436365de 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -80,9 +80,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def access_check
- # Use the magic string '_any' to indicate we do not know what the
- # changes are. This is also what gitlab-shell does.
- access.check(git_command, '_any')
+ access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index a10e159ea1e..8b33fa85c1e 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -13,7 +13,7 @@ class Projects::ImportsController < Projects::ApplicationController
end
def create
- if @project.update(safe_import_params)
+ if @project.update(import_params)
@project.import_state.reload.schedule
end
@@ -66,11 +66,11 @@ class Projects::ImportsController < Projects::ApplicationController
end
end
- def import_params
- params.require(:project).permit(:import_url)
+ def import_params_attributes
+ [:import_url]
end
- def safe_import_params
- import_params
+ def import_params
+ params.require(:project).permit(import_params_attributes)
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c6ab6b4642e..5ed46fc0545 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -268,7 +268,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
def set_suggested_issues_feature_flags
- push_frontend_feature_flag(:graphql)
- push_frontend_feature_flag(:issue_suggestions)
+ push_frontend_feature_flag(:graphql, default_enabled: true)
end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index c58b30eace7..bfbbcba883f 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
- before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
+ before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb
index ac1969adc6e..045a4e974fe 100644
--- a/app/controllers/projects/merge_requests/conflicts_controller.rb
+++ b/app/controllers/projects/merge_requests/conflicts_controller.rb
@@ -8,7 +8,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
def show
respond_to do |format|
format.html do
- labels
+ @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
end
format.json do
@@ -60,9 +60,15 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
end
end
+ private
+
def authorize_can_resolve_conflicts!
@conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request)
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end
+
+ def serializer
+ MergeRequestSerializer.new(current_user: current_user, project: project)
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index da9316d5f22..162c2636641 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -22,8 +22,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.html
format.json do
render json: {
- html: view_to_html_string("projects/merge_requests/_merge_requests"),
- labels: @labels.as_json(methods: :text_color)
+ html: view_to_html_string("projects/merge_requests/_merge_requests")
}
end
end
@@ -43,8 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count
-
- labels
+ @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
set_pipeline_variables
@@ -220,6 +218,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
head :ok
end
+ def discussions
+ merge_request.preload_discussions_diff_highlight
+
+ super
+ end
+
protected
alias_method :subscribable_resource, :merge_request
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index a860be83e95..c5454883060 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -15,6 +15,10 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
@protected_ref = @project.protected_branches.find(params[:id])
end
+ def access_levels
+ [:merge_access_levels, :push_access_levels]
+ end
+
def protected_ref_params
params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: access_level_attributes,
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index 3a3a29ddd0d..4e2a9df5576 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -32,7 +32,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
@protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref)
if @protected_ref.valid?
- render json: @protected_ref, status: :ok
+ render json: @protected_ref, status: :ok, include: access_levels
else
render json: @protected_ref.errors, status: :unprocessable_entity
end
@@ -62,6 +62,6 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def access_level_attributes
- %i(access_level id)
+ %i[access_level id]
end
end
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
index 01cedba95ac..41191639c2b 100644
--- a/app/controllers/projects/protected_tags_controller.rb
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -15,6 +15,10 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@protected_ref = @project.protected_tags.find(params[:id])
end
+ def access_levels
+ [:create_access_levels]
+ end
+
def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: access_level_attributes)
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 55827075896..62bdc84b41a 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -3,40 +3,17 @@
class Projects::ReleasesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
- before_action :authorize_push_code!
- before_action :tag
- before_action :release
+ before_action :authorize_read_release!
+ before_action :check_releases_page_feature_flag
- def edit
- end
-
- def update
- # Release belongs to Tag which is not active record object,
- # it exists only to save a description to each Tag.
- # If description is empty we should destroy the existing record.
- if release_params[:description].present?
- release.update(release_params)
- else
- release.destroy
- end
-
- redirect_to project_tag_path(@project, @tag.name)
+ def index
end
private
- def tag
- @tag ||= @repository.find_tag(params[:tag_id])
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def release
- @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
+ def check_releases_page_feature_flag
+ return render_404 unless Feature.enabled?(:releases_page, @project)
- def release_params
- params.require(:release).permit(:description)
+ push_frontend_feature_flag(:releases_page, @project)
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 30724de7f6a..ac3004d069f 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -5,7 +5,6 @@ module Projects
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :remote_mirror, only: [:show]
- before_action :check_cleanup_feature_flag!, only: :cleanup
def show
render_show
@@ -37,10 +36,6 @@ module Projects
private
- def check_cleanup_feature_flag!
- render_404 unless ::Feature.enabled?(:project_cleanup, project)
- end
-
def render_show
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
@deploy_tokens = @project.deploy_tokens.active
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index a44acb12bdf..255f1f3569a 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -75,7 +75,14 @@ class Projects::SnippetsController < Projects::ApplicationController
format.json do
render_blob_json(blob)
end
- format.js { render 'shared/snippets/show'}
+
+ format.js do
+ if @snippet.embeddable?
+ render 'shared/snippets/show'
+ else
+ head :not_found
+ end
+ end
end
end
diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb
new file mode 100644
index 00000000000..334e1847cc8
--- /dev/null
+++ b/app/controllers/projects/tags/releases_controller.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class Projects::Tags::ReleasesController < Projects::ApplicationController
+ # Authorize
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!
+ before_action :tag
+ before_action :release
+
+ def edit
+ end
+
+ def update
+ # Release belongs to Tag which is not active record object,
+ # it exists only to save a description to each Tag.
+ # If description is empty we should destroy the existing record.
+ if release_params[:description].present?
+ release.update(release_params)
+ else
+ release.destroy
+ end
+
+ redirect_to project_tag_path(@project, @tag.name)
+ end
+
+ private
+
+ def tag
+ @tag ||= @repository.find_tag(params[:tag_id])
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def release
+ @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def release_params
+ params.require(:release).permit(:description)
+ end
+end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 686d66b10a3..a17c050b696 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -42,10 +42,23 @@ class Projects::TagsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def create
- result = Tags::CreateService.new(@project, current_user)
- .execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
+ result = ::Tags::CreateService.new(@project, current_user)
+ .execute(params[:tag_name], params[:ref], params[:message])
if result[:status] == :success
+ # Release creation with Tags was deprecated in GitLab 11.7
+ if params[:release_description].present?
+ release_params = {
+ tag: params[:tag_name],
+ name: params[:tag_name],
+ description: params[:release_description]
+ }
+
+ Releases::CreateService
+ .new(@project, current_user, release_params)
+ .execute
+ end
+
@tag = result[:tag]
redirect_to project_tag_path(@project, @tag.name)
@@ -58,7 +71,7 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
- result = Tags::DestroyService.new(project, current_user).execute(params[:id])
+ result = ::Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format|
if result[:status] == :success
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8bf93bfd68d..878816475b2 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -19,6 +19,7 @@ class ProjectsController < Projects::ApplicationController
before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
+ before_action :authorize_download_code!, only: [:refs]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index 46e382e594e..8d1847507cc 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -15,7 +15,7 @@ module Sherlock
def destroy_all
Gitlab::Sherlock.collection.clear
- redirect_to :back, status: :found
+ redirect_back_or_default(options: { status: :found })
end
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dd9bf17cf0c..8ea5450b4e8 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -80,7 +80,13 @@ class SnippetsController < ApplicationController
render_blob_json(blob)
end
- format.js { render 'shared/snippets/show' }
+ format.js do
+ if @snippet.embeddable?
+ render 'shared/snippets/show'
+ else
+ head :not_found
+ end
+ end
end
end
diff --git a/app/finders/cluster_ancestors_finder.rb b/app/finders/cluster_ancestors_finder.rb
new file mode 100644
index 00000000000..2f9709ee057
--- /dev/null
+++ b/app/finders/cluster_ancestors_finder.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class ClusterAncestorsFinder
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(clusterable, current_user)
+ @clusterable = clusterable
+ @current_user = current_user
+ end
+
+ def execute
+ return [] unless can_read_clusters?
+
+ clusterable.clusters + ancestor_clusters
+ end
+
+ def has_ancestor_clusters?
+ ancestor_clusters.any?
+ end
+
+ private
+
+ attr_reader :clusterable, :current_user
+
+ def can_read_clusters?
+ Ability.allowed?(current_user, :read_cluster, clusterable)
+ end
+
+ # This unfortunately returns an Array, not a Relation!
+ def ancestor_clusters
+ strong_memoize(:ancestor_clusters) do
+ Clusters::Cluster.ancestor_clusters_for_clusterable(clusterable)
+ end
+ end
+end
diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb
index 220f62bcc7f..06ebb286086 100644
--- a/app/finders/concerns/finder_with_cross_project_access.rb
+++ b/app/finders/concerns/finder_with_cross_project_access.rb
@@ -5,7 +5,8 @@
#
# This module depends on the finder implementing the following methods:
#
-# - `#execute` should return an `ActiveRecord::Relation`
+# - `#execute` should return an `ActiveRecord::Relation` or the `model` needs to
+# be defined in the call to `requires_cross_project_access`.
# - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess
extend ActiveSupport::Concern
@@ -13,20 +14,35 @@ module FinderWithCrossProjectAccess
prepended do
extend Gitlab::CrossProjectAccess::ClassMethods
+
+ cattr_accessor :finder_model
+
+ def self.requires_cross_project_access(*args)
+ super
+
+ self.finder_model = extract_model_from_arguments(args)
+ end
+
+ private
+
+ def self.extract_model_from_arguments(args)
+ args.detect { |argument| argument.is_a?(Hash) && argument[:model] }
+ &.fetch(:model)
+ end
end
override :execute
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
- original = super
+ original = -> { super }
- return original unless check
- return original if should_skip_cross_project_check || can_read_cross_project?
+ return original.call unless check
+ return original.call if should_skip_cross_project_check || can_read_cross_project?
if check.should_run?(self)
- original.model.none
+ finder_model&.none || original.call.model.none
else
- original
+ original.call
end
end
@@ -48,8 +64,6 @@ module FinderWithCrossProjectAccess
skip_cross_project_check { super }
end
- private
-
attr_accessor :should_skip_cross_project_check
def skip_cross_project_check
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 8df01f1dad9..234b7090fd9 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -3,22 +3,27 @@
class EventsFinder
prepend FinderMethods
prepend FinderWithCrossProjectAccess
+
+ MAX_PER_PAGE = 100
+
attr_reader :source, :params, :current_user
- requires_cross_project_access unless: -> { source.is_a?(Project) }
+ requires_cross_project_access unless: -> { source.is_a?(Project) }, model: Event
# Used to filter Events
#
# Arguments:
# source - which user or project to looks for events on
# current_user - only return events for projects visible to this user
- # WARNING: does not consider project feature visibility!
# params:
# action: string
# target_type: string
# before: datetime
# after: datetime
- #
+ # per_page: integer (max. 100)
+ # page: integer
+ # with_associations: boolean
+ # sort: 'asc' or 'desc'
def initialize(params = {})
@source = params.delete(:source)
@current_user = params.delete(:current_user)
@@ -33,15 +38,18 @@ class EventsFinder
events = by_target_type(events)
events = by_created_at_before(events)
events = by_created_at_after(events)
+ events = sort(events)
+
+ events = events.with_associations if params[:with_associations]
- events
+ paginated_filtered_by_user_visibility(events)
end
private
# rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
- events.merge(ProjectsFinder.new(current_user: current_user).execute) # rubocop: disable CodeReuse/Finder
+ events.merge(Project.public_or_visible_to_user(current_user))
.joins(:project)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -77,4 +85,31 @@ class EventsFinder
events.where('events.created_at > ?', params[:after].end_of_day)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def sort(events)
+ return events unless params[:sort]
+
+ if params[:sort] == 'asc'
+ events.order_id_asc
+ else
+ events.order_id_desc
+ end
+ end
+
+ def paginated_filtered_by_user_visibility(events)
+ limited_events = events.page(page).per(per_page)
+ visible_events = limited_events.select { |event| event.visible_to_user?(current_user) }
+
+ Kaminari.paginate_array(visible_events, total_count: events.count)
+ end
+
+ def per_page
+ return MAX_PER_PAGE unless params[:per_page]
+
+ [params[:per_page], MAX_PER_PAGE].min
+ end
+
+ def page
+ params[:page] || 1
+ end
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index a9ce5be13f3..96a36db7ec8 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -112,7 +112,7 @@ class GroupDescendantsFinder
# rubocop: disable CodeReuse/ActiveRecord
def ancestors_of_groups(base_for_ancestors)
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
- Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
+ Gitlab::ObjectHierarchy.new(Group.where(id: group_ids))
.base_and_ancestors(upto: parent_group.id)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -132,7 +132,7 @@ class GroupDescendantsFinder
end
def subgroups
- return Group.none unless Group.supports_nested_groups?
+ return Group.none unless Group.supports_nested_objects?
# When filtering subgroups, we want to find all matches withing the tree of
# descendants to show to the user
@@ -183,7 +183,7 @@ class GroupDescendantsFinder
# rubocop: disable CodeReuse/ActiveRecord
def hierarchy_for_parent
- @hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id))
+ @hierarchy ||= Gitlab::ObjectHierarchy.new(Group.where(id: parent_group.id))
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index ea954f98220..0080123407d 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -46,7 +46,7 @@ class GroupsFinder < UnionFinder
return [Group.all] if current_user&.full_private_access? && all_available?
groups = []
- groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
+ groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects if current_user
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
groups << Group.none if groups.empty?
groups
@@ -66,7 +66,7 @@ class GroupsFinder < UnionFinder
.groups
.where('members.access_level >= ?', params[:min_access_level])
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(groups)
.base_and_descendants
end
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
new file mode 100644
index 00000000000..59e84198fde
--- /dev/null
+++ b/app/finders/releases_finder.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ReleasesFinder
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ def execute
+ return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
+
+ @project.releases.sorted
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 3f69af50f25..473c90c882c 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -11,7 +11,7 @@ module AppearancesHelper
end
def brand_image
- image_tag(current_appearance.logo) if current_appearance&.logo?
+ image_tag(current_appearance.logo_path) if current_appearance&.logo?
end
def brand_text
@@ -28,7 +28,7 @@ module AppearancesHelper
def brand_header_logo
if current_appearance&.header_logo?
- image_tag current_appearance.header_logo
+ image_tag current_appearance.header_logo_path
else
render 'shared/logo.svg'
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 74042f0bae8..82bb2d1a805 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -171,7 +171,6 @@ module ApplicationHelper
def page_filter_path(options = {})
without = options.delete(:without)
- add_label = options.delete(:label)
options = request.query_parameters.merge(options)
@@ -181,11 +180,7 @@ module ApplicationHelper
end
end
- params = options.compact
-
- params.delete(:label_name) unless add_label
-
- "#{request.path}?#{params.to_param}"
+ "#{request.path}?#{options.compact.to_param}"
end
def outdated_browser?
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 086bb38ce9a..c8e4e2e3df9 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -20,12 +20,24 @@ module ApplicationSettingsHelper
def enabled_protocol
case Gitlab::CurrentSettings.enabled_git_access_protocol
when 'http'
- gitlab_config.protocol
+ Gitlab.config.gitlab.protocol
when 'ssh'
'ssh'
end
end
+ def all_protocols_enabled?
+ Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
+ end
+
+ def ssh_enabled?
+ all_protocols_enabled? || enabled_protocol == 'ssh'
+ end
+
+ def http_enabled?
+ all_protocols_enabled? || Gitlab::CurrentSettings.enabled_git_access_protocol == 'http'
+ end
+
def enabled_project_button(project, protocol)
case protocol
when 'ssh'
@@ -218,7 +230,8 @@ module ApplicationSettingsHelper
:version_check_enabled,
:web_ide_clientside_preview_enabled,
:diff_max_patch_bytes,
- :commit_email_hostname
+ :commit_email_hostname,
+ :protected_ci_variables
]
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index bd42f00944f..23d6684a8e6 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -140,36 +140,6 @@ module BlobHelper
Gitlab::Sanitizers::SVG.clean(data)
end
- # Remove once https://gitlab.com/gitlab-org/gitlab-ce/issues/36103 is closed
- # and :workhorse_set_content_type flag is removed
- # If we blindly set the 'real' content type when serving a Git blob we
- # are enabling XSS attacks. An attacker could upload e.g. a Javascript
- # file to a Git repository, trick the browser of a victim into
- # downloading the blob, and then the 'application/javascript' content
- # type would tell the browser to execute the attacker's Javascript. By
- # overriding the content type and setting it to 'text/plain' (in the
- # example of Javascript) we tell the browser of the victim not to
- # execute untrusted data.
- def safe_content_type(blob)
- if blob.extension == 'svg'
- blob.mime_type
- elsif blob.text?
- 'text/plain; charset=utf-8'
- elsif blob.image?
- blob.content_type
- else
- 'application/octet-stream'
- end
- end
-
- def content_disposition(blob, inline)
- # Remove the following line when https://gitlab.com/gitlab-org/gitlab-ce/issues/36103
- # is closed and :workhorse_set_content_type flag is removed
- return 'attachment' if blob.extension == 'svg'
-
- inline ? 'inline' : 'attachment'
- end
-
def ref_project
@ref_project ||= @target_project || @project
end
@@ -207,7 +177,8 @@ module BlobHelper
'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-filename' => @blob && @blob.path,
- 'project-id' => project.id
+ 'project-id' => project.id,
+ 'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path)
}
end
@@ -223,7 +194,7 @@ module BlobHelper
def open_raw_blob_button(blob)
return if blob.empty?
- return if blob.raw_binary? || blob.stored_externally?
+ return if blob.binary? || blob.stored_externally?
title = 'Open raw'
link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
new file mode 100644
index 00000000000..e3728804c2a
--- /dev/null
+++ b/app/helpers/ci_variables_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module CiVariablesHelper
+ def ci_variable_protected_by_default?
+ Gitlab::CurrentSettings.current_application_settings.protected_ci_variables
+ end
+
+ def ci_variable_protected?(variable, only_key_value)
+ if variable && !only_key_value
+ variable.protected
+ else
+ ci_variable_protected_by_default?
+ end
+ end
+end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b6844d36052..32431959851 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -138,30 +138,6 @@ module DiffHelper
!diff_file.deleted_file? && @merge_request && @merge_request.source_project
end
- def diff_render_error_reason(viewer)
- case viewer.render_error
- when :too_large
- "it is too large"
- when :server_side_but_stored_externally
- case viewer.diff_file.external_storage
- when :lfs
- 'it is stored in LFS'
- else
- 'it is stored externally'
- end
- end
- end
-
- def diff_render_error_options(viewer)
- diff_file = viewer.diff_file
- options = []
-
- blob_url = project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.file_path))
- options << link_to('view the blob', blob_url)
-
- options
- end
-
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file?
"file-deletion"
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index e4c46ceeaa2..fa5d3ae474a 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -58,7 +58,7 @@ module EmailsHelper
def header_logo
if current_appearance&.header_logo?
image_tag(
- current_appearance.header_logo,
+ current_appearance.header_logo_path,
style: 'height: 50px'
)
else
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 866fc555856..4a9ed123161 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -126,7 +126,7 @@ module GroupsHelper
end
def supports_nested_groups?
- Group.supports_nested_groups?
+ Group.supports_nested_objects?
end
private
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index da991458ea7..5f7147508c7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -23,30 +23,41 @@ module IssuablesHelper
end
end
- def sidebar_due_date_tooltip_label(issuable)
- if issuable.due_date
- "#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
- else
- _('Due date')
- end
+ def sidebar_milestone_tooltip_label(milestone)
+ return _('Milestone') unless milestone.present?
+
+ [milestone[:title], sidebar_milestone_remaining_days(milestone) || _('Milestone')].join('<br/>')
+ end
+
+ def sidebar_milestone_remaining_days(milestone)
+ due_date_with_remaining_days(milestone[:due_date], milestone[:start_date])
+ end
+
+ def sidebar_due_date_tooltip_label(due_date)
+ [_('Due date'), due_date_with_remaining_days(due_date)].compact.join('<br/>')
end
- def due_date_remaining_days(issuable)
- remaining_days_in_words = remaining_days_in_words(issuable)
+ def due_date_with_remaining_days(due_date, start_date = nil)
+ return unless due_date
- "#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
+ "#{due_date.to_s(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
+ end
+
+ def sidebar_label_filter_path(base_path, label_name)
+ query_params = { label_name: [label_name] }.to_query
+
+ "#{base_path}?#{query_params}"
end
def multi_label_name(current_labels, default_label)
- if current_labels && current_labels.any?
- title = current_labels.first.try(:title)
- if current_labels.size > 1
- "#{title} +#{current_labels.size - 1} more"
- else
- title
- end
+ return default_label if current_labels.blank?
+
+ title = current_labels.first.try(:title) || current_labels.first[:title]
+
+ if current_labels.size > 1
+ "#{title} +#{current_labels.size - 1} more"
else
- default_label
+ title
end
end
@@ -197,19 +208,11 @@ module IssuablesHelper
output.join.html_safe
end
- # rubocop: disable CodeReuse/ActiveRecord
- def issuable_todo(issuable)
- if current_user
- current_user.todos.find_by(target: issuable, state: :pending)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
if labels && labels.any?
- label_names = first.collect(&:name)
+ label_names = first.collect { |label| label.fetch(:title) }
label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ')
@@ -356,12 +359,6 @@ module IssuablesHelper
issuable.model_name.human.downcase
end
- def selected_labels
- Array(params[:label_name]).map do |label_name|
- Label.new(title: label_name)
- end
- end
-
def has_filter_bar_param?
finder.class.scalar_params.any? { |p| params[p].present? }
end
@@ -386,19 +383,20 @@ module IssuablesHelper
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
- def issuable_todo_button_data(issuable, todo, is_collapsed)
+ def issuable_todo_button_data(issuable, is_collapsed)
{
- todo_text: "Add todo",
- mark_text: "Mark todo as done",
- todo_icon: (is_collapsed ? sprite_icon('todo-add') : nil),
- mark_icon: (is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : nil),
- issuable_id: issuable.id,
- issuable_type: issuable.class.name.underscore,
- url: project_todos_path(@project),
- delete_path: (dashboard_todo_path(todo) if todo),
- placement: (is_collapsed ? 'left' : nil),
- container: (is_collapsed ? 'body' : nil),
- boundary: 'viewport'
+ todo_text: _('Add todo'),
+ mark_text: _('Mark todo as done'),
+ todo_icon: sprite_icon('todo-add'),
+ mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'),
+ issuable_id: issuable[:id],
+ issuable_type: issuable[:type],
+ create_path: issuable[:create_todo_path],
+ delete_path: issuable.dig(:current_user, :todo, :delete_path),
+ placement: is_collapsed ? 'left' : nil,
+ container: is_collapsed ? 'body' : nil,
+ boundary: 'viewport',
+ is_collapsed: is_collapsed
}
end
@@ -418,27 +416,20 @@ module IssuablesHelper
end
end
- def issuable_sidebar_options(issuable, can_edit_issuable)
+ def issuable_sidebar_options(issuable)
{
- endpoint: "#{issuable_json_path(issuable)}?serializer=sidebar",
- toggleSubscriptionEndpoint: toggle_subscription_path(issuable),
- moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
- projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
- editable: can_edit_issuable,
- currentUser: UserSerializer.new.represent(current_user),
+ endpoint: "#{issuable[:issuable_json_path]}?serializer=sidebar_extras",
+ toggleSubscriptionEndpoint: issuable[:toggle_subscription_path],
+ moveIssueEndpoint: issuable[:move_issue_path],
+ projectsAutocompleteEndpoint: issuable[:projects_autocomplete_path],
+ editable: issuable.dig(:current_user, :can_edit),
+ currentUser: issuable[:current_user],
rootPath: root_path,
- fullPath: @project.full_path
+ fullPath: issuable[:project_full_path]
}
end
def parent
@project || @group
end
-
- def issuable_milestone_tooltip_title(issuable)
- if issuable.milestone
- milestone_tooltip = milestone_tooltip_title(issuable.milestone)
- _('Milestone') + (milestone_tooltip ? ': ' + milestone_tooltip : '')
- end
- end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 9666080092b..50aec83b867 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -114,12 +114,6 @@ module MilestonesHelper
end
end
- def milestone_tooltip_title(milestone)
- if milestone
- "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
- end
- end
-
def milestone_time_for(date, date_type)
title = date_type == :start ? "Start date" : "End date"
@@ -173,7 +167,7 @@ module MilestonesHelper
def milestone_tooltip_due_date(milestone)
if milestone.due_date
- "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
+ "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone.due_date, milestone.start_date)})"
else
_('Milestone')
end
@@ -237,12 +231,15 @@ module MilestonesHelper
end
end
- def group_or_dashboard_milestone_path(milestone)
- if milestone.group_milestone?
- group_milestone_path(milestone.group, milestone.iid, milestone: { title: milestone.title })
- else
- dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- end
+ def group_or_project_milestone_path(milestone)
+ params =
+ if milestone.group_milestone?
+ { milestone: { title: milestone.title } }
+ else
+ { title: milestone.title }
+ end
+
+ milestone_path(milestone.milestone, params)
end
def can_admin_project_milestones?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 1186eb3ddcc..0cfc2db3285 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -271,13 +271,27 @@ module ProjectsHelper
params[:legacy_render] ? { markdown_engine: :redcarpet } : {}
end
+ def explore_projects_tab?
+ current_page?(explore_projects_path) ||
+ current_page?(trending_explore_projects_path) ||
+ current_page?(starred_explore_projects_path)
+ end
+
+ def show_merge_request_count?(disabled: false, compact_mode: false)
+ !disabled && !compact_mode && Feature.enabled?(:project_list_show_mr_count, default_enabled: true)
+ end
+
+ def show_issue_count?(disabled: false, compact_mode: false)
+ !disabled && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
+ end
+
private
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
if !project.empty_repo? && can?(current_user, :download_code, project)
- nav_tabs << [:files, :commits, :network, :graphs, :forks]
+ nav_tabs << [:files, :commits, :network, :graphs, :forks, :releases]
end
if project.repo_exists? && can?(current_user, :read_merge_request, project)
@@ -515,24 +529,11 @@ module ProjectsHelper
end
end
- def explore_projects_tab?
- current_page?(explore_projects_path) ||
- current_page?(trending_explore_projects_path) ||
- current_page?(starred_explore_projects_path)
- end
-
- def show_merge_request_count?(merge_requests, compact_mode)
- merge_requests && !compact_mode && Feature.enabled?(:project_list_show_mr_count, default_enabled: true)
- end
-
- def show_issue_count?(issues, compact_mode)
- issues && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
- end
-
def sidebar_projects_paths
%w[
projects#show
projects#activity
+ releases#index
cycle_analytics#show
]
end
@@ -564,7 +565,6 @@ module ProjectsHelper
projects/repositories
tags
branches
- releases
graphs
network
]
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 80cc568820a..0ee76a51f7d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -24,10 +24,10 @@ module SearchHelper
end
def search_entries_info(collection, scope, term)
- return unless collection.count > 0
+ return if collection.to_a.empty?
from = collection.offset_value + 1
- to = collection.offset_value + collection.count
+ to = collection.offset_value + collection.to_a.size
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index c7d31f3469d..ecb2b2d707b 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -110,7 +110,7 @@ module SnippetsHelper
def embedded_snippet_raw_button
blob = @snippet.blob
- return if blob.empty? || blob.raw_binary? || blob.stored_externally?
+ return if blob.empty? || blob.binary? || blob.stored_externally?
snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
raw_snippet_url(@snippet)
@@ -130,12 +130,4 @@ module SnippetsHelper
link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
end
-
- def public_snippet?
- if @snippet.project_id?
- can?(nil, :read_project_snippet, @snippet)
- else
- can?(nil, :read_personal_snippet, @snippet)
- end
- end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 67c808b167a..02762897c89 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -159,27 +159,28 @@ module SortingHelper
sort_options_hash[sort_value]
end
+ def issuable_sort_icon_suffix(sort_value)
+ case sort_value
+ when sort_value_milestone, sort_value_due_date, /_asc\z/
+ 'lowest'
+ else
+ 'highest'
+ end
+ end
+
def issuable_sort_direction_button(sort_value)
link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
reverse_sort = issuable_reverse_sort_order_hash[sort_value]
if reverse_sort
- reverse_url = page_filter_path(sort: reverse_sort, label: true)
+ reverse_url = page_filter_path(sort: reverse_sort)
else
reverse_url = '#'
link_class += ' disabled'
end
link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do
- icon_suffix =
- case sort_value
- when sort_value_milestone, sort_value_due_date, /_asc\z/
- 'lowest'
- else
- 'highest'
- end
-
- sprite_icon("sort-#{icon_suffix}", size: 16)
+ sprite_icon("sort-#{issuable_sort_icon_suffix(sort_value)}", size: 16)
end
end
@@ -233,7 +234,7 @@ module SortingHelper
end
def sort_title_milestone
- s_('SortOptions|Milestone')
+ s_('SortOptions|Milestone due date')
end
def sort_title_milestone_later
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index bde9ca0cbf2..73c1402eae5 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -8,7 +8,7 @@ module UsersHelper
end
def user_email_help_text(user)
- return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present?
+ return 'We also use email for avatar detection if no avatar is uploaded' unless user.unconfirmed_email.present?
confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index ab77b149072..5e519cf5c19 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -6,8 +6,7 @@ module VersionCheckHelper
return unless Gitlab::CurrentSettings.version_check_enabled
return if User.single_user&.requires_usage_stats_consent?
- image_url = VersionCheck.new.url
- image_tag image_url, class: 'js-version-status-badge'
+ image_tag VersionCheck.url, class: 'js-version-status-badge'
end
def link_to_version
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index e9fc39e451b..bb5b1555dc4 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -7,8 +7,7 @@ module WorkhorseHelper
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
- headers['Content-Disposition'] = content_disposition(blob, inline)
- headers['Content-Type'] = safe_content_type(blob)
+ headers['Content-Disposition'] = inline ? 'inline' : 'attachment'
# If enabled, this will override the values set above
workhorse_set_content_type!
@@ -47,6 +46,6 @@ module WorkhorseHelper
end
def workhorse_set_content_type!
- headers[Gitlab::Workhorse::DETECT_HEADER] = "true" if Feature.enabled?(:workhorse_set_content_type)
+ headers[Gitlab::Workhorse::DETECT_HEADER] = "true"
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 93b51fb1774..370e6d2f90b 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -56,7 +56,9 @@ module Emails
@milestone = milestone
@milestone_url = milestone_url(@milestone)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason).merge({
+ template_name: 'changed_milestone_email'
+ }))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 6524d0c2087..9ba8f92fcbf 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -51,7 +51,9 @@ module Emails
@milestone = milestone
@milestone_url = milestone_url(@milestone)
- mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason).merge({
+ template_name: 'changed_milestone_email'
+ }))
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 15710bee4d4..efa1233b434 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
include Emails::AutoDevops
include Emails::RemoteMirrors
+ helper MilestonesHelper
helper MergeRequestsHelper
helper DiffHelper
helper BlobHelper
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index bffba3e13fa..e114c435b67 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -28,4 +28,32 @@ class Appearance < ActiveRecord::Base
errors.add(:single_appearance_row, 'Only 1 appearances row can exist')
end
end
+
+ def logo_path
+ logo_system_path(logo, 'logo')
+ end
+
+ def header_logo_path
+ logo_system_path(header_logo, 'header_logo')
+ end
+
+ def favicon_path
+ logo_system_path(favicon, 'favicon')
+ end
+
+ private
+
+ def logo_system_path(logo, mount_type)
+ return unless logo&.upload
+
+ # If we're using a CDN, we need to use the full URL
+ asset_host = ActionController::Base.asset_host
+ local_path = Gitlab::Routing.url_helpers.appearance_upload_path(
+ filename: logo.filename,
+ id: logo.upload.model_id,
+ model: 'appearance',
+ mounted_as: mount_type)
+
+ Gitlab::Utils.append_path(asset_host, local_path)
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 4319db42019..88746375c67 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -302,7 +302,8 @@ class ApplicationSetting < ActiveRecord::Base
user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
- commit_email_hostname: default_commit_email_hostname
+ commit_email_hostname: default_commit_email_hostname,
+ protected_ci_variables: false
}
end
@@ -311,7 +312,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
- create(defaults)
+ build_from_defaults.tap(&:save)
end
def self.human_attribute_name(attr, _options = {})
@@ -382,7 +383,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def restricted_visibility_levels=(levels)
- super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
+ super(levels&.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
def strip_sentry_values
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 66a0925c495..c5766eb0327 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -102,7 +102,7 @@ class Blob < SimpleDelegator
# If the blob is a text based blob the content is converted to UTF-8 and any
# invalid byte sequences are replaced.
def data
- if binary?
+ if binary_in_repo?
super
else
@data ||= super.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
@@ -149,7 +149,7 @@ class Blob < SimpleDelegator
# an LFS pointer, we assume the file stored in LFS is binary, unless a
# text-based rich blob viewer matched on the file's extension. Otherwise, this
# depends on the type of the blob itself.
- def raw_binary?
+ def binary?
if stored_externally?
if rich_viewer
rich_viewer.binary?
@@ -161,7 +161,7 @@ class Blob < SimpleDelegator
true
end
else
- binary?
+ binary_in_repo?
end
end
@@ -180,7 +180,7 @@ class Blob < SimpleDelegator
end
def readable_text?
- text? && !stored_externally? && !truncated?
+ text_in_repo? && !stored_externally? && !truncated?
end
def simple_viewer
@@ -220,7 +220,7 @@ class Blob < SimpleDelegator
def simple_viewer_class
if empty?
BlobViewer::Empty
- elsif raw_binary?
+ elsif binary?
BlobViewer::Download
else # text
BlobViewer::Text
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index eaaf9af1330..df6b9bb2f0b 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -16,7 +16,7 @@ module BlobViewer
def initialize(blob)
@blob = blob
- @initially_binary = blob.binary?
+ @initially_binary = blob.binary_in_repo?
end
def self.partial_path
@@ -52,7 +52,7 @@ module BlobViewer
end
def self.can_render?(blob, verify_binary: true)
- return false if verify_binary && binary? != blob.binary?
+ return false if verify_binary && binary? != blob.binary_in_repo?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(blob.file_type)
@@ -72,7 +72,7 @@ module BlobViewer
end
def binary_detected_after_load?
- !@initially_binary && blob.binary?
+ !@initially_binary && blob.binary_in_repo?
end
# This method is used on the server side to check whether we can attempt to
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 277f7c2717c..2d237383e60 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -22,49 +22,30 @@ class BroadcastMessage < ActiveRecord::Base
after_commit :flush_redis_cache
def self.current
- raw_messages = Rails.cache.fetch(CACHE_KEY, expires_in: cache_expires_in) do
+ messages = cache.fetch(CACHE_KEY, as: BroadcastMessage, expires_in: cache_expires_in) do
remove_legacy_cache_key
- current_and_future_messages.to_json
+ current_and_future_messages
end
- messages = decode_messages(raw_messages)
-
return [] unless messages&.present?
now_or_future = messages.select(&:now_or_future?)
# If there are cached entries but none are to be displayed we'll purge the
# cache so we don't keep running this code all the time.
- Rails.cache.delete(CACHE_KEY) if now_or_future.empty?
+ cache.expire(CACHE_KEY) if now_or_future.empty?
now_or_future.select(&:now?)
end
- def self.decode_messages(raw_messages)
- return unless raw_messages&.present?
-
- message_list = ActiveSupport::JSON.decode(raw_messages)
-
- return unless message_list.is_a?(Array)
-
- valid_attr = BroadcastMessage.attribute_names
-
- message_list.map do |raw|
- BroadcastMessage.new(raw) if valid_cache_entry?(raw, valid_attr)
- end.compact
- rescue ActiveSupport::JSON.parse_error
- end
-
- def self.valid_cache_entry?(raw, valid_attr)
- return false unless raw.is_a?(Hash)
-
- (raw.keys - valid_attr).empty?
- end
-
def self.current_and_future_messages
where('ends_at > :now', now: Time.zone.now).order_id_asc
end
+ def self.cache
+ Gitlab::JsonCache.new(cache_key_with_version: false)
+ end
+
def self.cache_expires_in
nil
end
@@ -74,7 +55,7 @@ class BroadcastMessage < ActiveRecord::Base
# environment a one-shot migration would not work because the cache
# would be repopulated by a node that has not been upgraded.
def self.remove_legacy_cache_key
- Rails.cache.delete(LEGACY_CACHE_KEY)
+ cache.expire(LEGACY_CACHE_KEY)
end
def active?
@@ -102,7 +83,7 @@ class BroadcastMessage < ActiveRecord::Base
end
def flush_redis_cache
- Rails.cache.delete(CACHE_KEY)
+ self.class.cache.expire(CACHE_KEY)
self.class.remove_legacy_cache_key
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e2917049902..aeb35538d67 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -10,6 +10,7 @@ module Ci
include Importable
include Gitlab::Utils::StrongMemoize
include Deployable
+ include HasRef
belongs_to :project, inverse_of: :builds
belongs_to :runner
@@ -220,6 +221,10 @@ module Ci
next unless build.project
build.deployment&.drop
+ end
+
+ after_transition any => [:failed] do |build|
+ next unless build.project
if build.retry_failure?
begin
@@ -640,11 +645,11 @@ module Ci
def secret_group_variables
return [] unless project.group
- project.group.ci_variables_for(ref, project)
+ project.group.ci_variables_for(git_ref, project)
end
def secret_project_variables(environment: persisted_environment)
- project.ci_variables_for(ref: ref, environment: environment)
+ project.ci_variables_for(ref: git_ref, environment: environment)
end
def steps
@@ -840,6 +845,7 @@ module Ci
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d06022a0fb7..01134e133db 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -11,6 +11,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include AtomicInternalId
include EnumWithNil
+ include HasRef
belongs_to :project, inverse_of: :all_pipelines
belongs_to :user
@@ -56,11 +57,7 @@ module Ci
validates :tag, inclusion: { in: [false], if: :merge_request? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
-
- # Replace validator below with
- # `validates :source, presence: { unless: :importing? }, on: :create`
- # when removing Gitlab.rails5? code.
- validate :valid_source, unless: :importing?, on: :create
+ validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
after_create :keep_around_commits, unless: :importing?
@@ -68,11 +65,7 @@ module Ci
# this `Hash` with new values.
enum_with_nil source: ::Ci::PipelineEnums.sources
- enum_with_nil config_source: {
- unknown_source: nil,
- repository_source: 1,
- auto_devops_source: 2
- }
+ enum_with_nil config_source: ::Ci::PipelineEnums.config_sources
# We use `Ci::PipelineEnums.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
@@ -388,7 +381,7 @@ module Ci
end
def branch?
- !tag? && !merge_request?
+ super && !merge_request?
end
def stuck?
@@ -588,7 +581,7 @@ module Ci
end
def protected_ref?
- strong_memoize(:protected_ref) { project.protected_for?(ref) }
+ strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
end
def legacy_trigger
@@ -642,7 +635,7 @@ module Ci
def all_merge_requests
@all_merge_requests ||=
if merge_request?
- project.merge_requests.where(id: merge_request.id)
+ project.merge_requests.where(id: merge_request_id)
else
project.merge_requests.where(source_branch: ref)
end
@@ -720,14 +713,16 @@ module Ci
end
def git_ref
- if branch?
- Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
- elsif merge_request?
+ if merge_request?
+ ##
+ # In the future, we're going to change this ref to
+ # merge request's merged reference, such as "refs/merge-requests/:iid/merge".
+ # In order to do that, we have to update GitLab-Runner's source pulling
+ # logic.
+ # See https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1092
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
- elsif tag?
- Gitlab::Git::TAG_REF_PREFIX + ref.to_s
else
- raise ArgumentError, 'Invalid pipeline type!'
+ super
end
end
@@ -742,11 +737,5 @@ module Ci
project.repository.keep_around(self.sha, self.before_sha)
end
-
- def valid_source
- if source.nil? || source == "unknown"
- errors.add(:source, "invalid source")
- end
- end
end
end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index c0f16066e0b..2994aaae4aa 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -25,5 +25,15 @@ module Ci
merge_request: 10
}
end
+
+ # Returns the `Hash` to use for creating the `config_sources` enum for
+ # `Ci::Pipeline`.
+ def self.config_sources
+ {
+ unknown_source: nil,
+ repository_source: 1,
+ auto_devops_source: 2
+ }
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 2693386443a..8249199e76f 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -58,8 +58,7 @@ module Ci
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
scope :deprecated_shared, -> { instance_type }
- # this should get replaced with `project_type.or(group_type)` once using Rails5
- scope :deprecated_specific, -> { where(runner_type: [runner_types[:project_type], runner_types[:group_type]]) }
+ scope :deprecated_specific, -> { project_type.or(group_type) }
scope :belonging_to_project, -> (project_id) {
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
@@ -67,7 +66,7 @@ module Ci
scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
- hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
+ hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
joins(:groups).where(namespaces: { id: hierarchy_groups })
}
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 74ef7c7e145..c758577815a 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class CertManager < ActiveRecord::Base
- VERSION = 'v0.5.0'.freeze
+ VERSION = 'v0.5.2'.freeze
self.table_name = 'clusters_applications_cert_managers'
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 8f8790585a3..7799f069742 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -23,7 +23,7 @@ module Clusters
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
- before_transition any => [:installed] do |application|
+ after_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 168a24da738..0a3168afe68 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -3,9 +3,9 @@
module Clusters
module Applications
class Knative < ActiveRecord::Base
- VERSION = '0.1.3'.freeze
+ VERSION = '0.2.2'.freeze
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
-
+ METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze
FETCH_IP_ADDRESS_DELAY = 30.seconds
self.table_name = 'clusters_applications_knative'
@@ -20,7 +20,7 @@ module Clusters
self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
state_machine :status do
- before_transition any => [:installed] do |application|
+ after_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
@@ -49,7 +49,8 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: REPOSITORY
+ repository: REPOSITORY,
+ postinstall: install_knative_metrics
)
end
@@ -94,6 +95,10 @@ module Clusters
rescue Kubeclient::ResourceNotFoundError
[]
end
+
+ def install_knative_metrics
+ ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
+ end
end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 46d0388a464..e25be522d68 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -50,7 +50,8 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files
+ files: files,
+ postinstall: install_knative_metrics
)
end
@@ -74,6 +75,10 @@ module Clusters
def kube_client
cluster&.kubeclient&.core_client
end
+
+ def install_knative_metrics
+ ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available?
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index c931b340b24..0c0247da1fb 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.39'.freeze
+ VERSION = '0.1.43'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7fe43cd2de0..6050955fbd8 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -63,6 +63,7 @@ module Clusters
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
+ delegate :available?, to: :application_knative, prefix: true, allow_nil: true
enum cluster_type: {
instance_type: 1,
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 867f0edcb07..1cc170c8c4d 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -65,6 +65,8 @@ module Clusters
abac: 2
}
+ default_value_for :authorization_type, :rbac
+
def actual_namespace
if namespace.present?
namespace
@@ -106,7 +108,7 @@ module Clusters
def terminals(environment)
with_reactive_cache do |data|
pods = filter_by_label(data[:pods], app: environment.slug)
- terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }
+ terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.compact
terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -228,7 +230,7 @@ module Clusters
return unless namespace_changed?
run_after_commit do
- ClusterPlatformConfigureWorker.perform_async(cluster_id)
+ ClusterConfigureWorker.perform_async(cluster_id)
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index a422a0995ff..01f4c58daa1 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -469,6 +469,10 @@ class Commit
!!merged_merge_request(user)
end
+ def cache_key
+ "commit:#{sha}"
+ end
+
private
def commit_reference(from, referable_commit_id, full: false)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index b42236c1fa2..4687ec7d166 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -43,7 +43,18 @@ module Avatarable
end
def avatar_path(only_path: true, size: nil)
- return unless self[:avatar].present?
+ unless self.try(:id)
+ return uncached_avatar_path(only_path: only_path, size: size)
+ end
+
+ # Cache this avatar path only within the request because avatars in
+ # object storage may be generated with time-limited, signed URLs.
+ key = "#{self.class.name}:#{self.id}:#{only_path}:#{size}"
+ Gitlab::SafeRequestStore[key] ||= uncached_avatar_path(only_path: only_path, size: size)
+ end
+
+ def uncached_avatar_path(only_path: true, size: nil)
+ return unless self.try(:avatar).present?
asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present?
diff --git a/app/models/concerns/blob_like.rb b/app/models/concerns/blob_like.rb
index f20f01486a5..dc80f8d62f4 100644
--- a/app/models/concerns/blob_like.rb
+++ b/app/models/concerns/blob_like.rb
@@ -28,7 +28,7 @@ module BlobLike
nil
end
- def binary?
+ def binary_in_repo?
false
end
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index 75592bb63e2..3d60f6924c1 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -23,7 +23,12 @@ module CacheableAttributes
end
def build_from_defaults(attributes = {})
- new(defaults.merge(attributes))
+ final_attributes = defaults
+ .merge(attributes)
+ .stringify_keys
+ .slice(*column_names)
+
+ new(final_attributes)
end
def cached
diff --git a/app/models/concerns/descendant.rb b/app/models/concerns/descendant.rb
new file mode 100644
index 00000000000..4c436522122
--- /dev/null
+++ b/app/models/concerns/descendant.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Descendant
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def supports_nested_objects?
+ Gitlab::Database.postgresql?
+ end
+ end
+end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 266c37fa3a1..e4e5928f5cf 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -9,7 +9,7 @@ module DiscussionOnDiff
included do
delegate :line_code,
:original_line_code,
- :diff_file,
+ :note_diff_file,
:diff_line,
:active?,
:created_at_diff?,
@@ -39,6 +39,7 @@ module DiscussionOnDiff
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true, diff_limit: nil)
+ return [] unless on_text?
return [] if diff_line.nil? && first_note.is_a?(LegacyDiffNote)
diff_limit = [diff_limit, NUMBER_OF_TRUNCATED_DIFF_LINES].compact.min
@@ -59,6 +60,13 @@ module DiscussionOnDiff
prev_lines
end
+ def diff_file
+ strong_memoize(:diff_file) do
+ # Falling back here is important as `note_diff_files` are created async.
+ fetch_preloaded_diff_file || first_note.diff_file
+ end
+ end
+
def line_code_in_diffs(diff_refs)
if active?(diff_refs)
line_code
@@ -66,4 +74,15 @@ module DiscussionOnDiff
original_line_code
end
end
+
+ private
+
+ def fetch_preloaded_diff_file
+ fetch_preloaded_diff =
+ context_noteable &&
+ context_noteable.preloads_discussion_diff_highlighting? &&
+ note_diff_file
+
+ context_noteable.discussions_diffs.find_by_id(note_diff_file.id) if fetch_preloaded_diff
+ end
end
diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb
index 23acfe9a55f..6d0a21cf070 100644
--- a/app/models/concerns/enum_with_nil.rb
+++ b/app/models/concerns/enum_with_nil.rb
@@ -16,7 +16,7 @@ module EnumWithNil
# E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
# this overrides auto-generated method `unknown_failure?`
define_method("#{key_with_nil}?") do
- Gitlab.rails5? ? self[name].nil? : super()
+ self[name].nil?
end
# E.g. for enum_with_nil failure_reason: { unknown_failure: nil }
@@ -24,7 +24,6 @@ module EnumWithNil
define_method(name) do
orig = super()
- return orig unless Gitlab.rails5?
return orig unless orig.nil?
self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb
new file mode 100644
index 00000000000..d7089294efc
--- /dev/null
+++ b/app/models/concerns/has_ref.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module HasRef
+ extend ActiveSupport::Concern
+
+ def branch?
+ !tag?
+ end
+
+ def git_ref
+ if branch?
+ Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
+ elsif tag?
+ Gitlab::Git::TAG_REF_PREFIX + ref.to_s
+ end
+ end
+end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e44a069b730..055ffe04646 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -42,7 +42,7 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
IssuesFinder.new(user, issues_finder_params)
- .execute.preload(:assignees).where(milestone_id: milestoneish_ids)
+ .execute.preload(:assignees).where(milestone_id: milestoneish_id)
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index eb315058c3a..29476654bf7 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -26,10 +26,18 @@ module Noteable
DiscussionNote.noteable_types.include?(base_class_name)
end
+ def supports_suggestion?
+ false
+ end
+
def discussions_rendered_on_frontend?
false
end
+ def preloads_discussion_diff_highlighting?
+ false
+ end
+
def discussion_notes
notes
end
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index 69554f18ea2..4bb4ffe2a8e 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -49,10 +49,6 @@ module RedisCacheable
end
def cast_value_from_cache(attribute, value)
- if Gitlab.rails5?
- self.class.type_for_attribute(attribute.to_s).cast(value)
- else
- self.class.column_for_attribute(attribute).type_cast_from_database(value)
- end
+ self.class.type_for_attribute(attribute.to_s).cast(value)
end
end
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index 32e8104125c..9bcc95e35a5 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -5,9 +5,8 @@ class DashboardGroupMilestone < GlobalMilestone
attr_reader :group_name
- override :initialize
def initialize(milestone)
- super(milestone.title, Array(milestone))
+ super
@group_name = milestone.group.full_name
end
@@ -19,22 +18,4 @@ class DashboardGroupMilestone < GlobalMilestone
.active
.map { |m| new(m) }
end
-
- override :group_milestone?
- def group_milestone?
- @first_milestone.group_milestone?
- end
-
- override :milestoneish_ids
- def milestoneish_ids
- milestones.map(&:id)
- end
-
- def group
- @first_milestone.group
- end
-
- def iid
- @first_milestone.iid
- end
end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
index 96bc8090b81..9b377b70e5b 100644
--- a/app/models/dashboard_milestone.rb
+++ b/app/models/dashboard_milestone.rb
@@ -1,11 +1,15 @@
# frozen_string_literal: true
class DashboardMilestone < GlobalMilestone
- def issues_finder_params
- { authorized_only: true }
+ attr_reader :project_name
+
+ def initialize(milestone)
+ super
+
+ @project_name = milestone.project.full_name
end
- def dashboard_milestone?
+ def project_milestone?
true
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c32008aa9c7..279603496b0 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -66,10 +66,23 @@ class DiffNote < Note
self.original_position.diff_refs == diff_refs
end
+ def supports_suggestion?
+ return false unless noteable.supports_suggestion? && on_text?
+ # We don't want to trigger side-effects of `diff_file` call.
+ return false unless file = fetch_diff_file
+ return false unless line = file.line_for_position(self.original_position)
+
+ line&.suggestible?
+ end
+
def discussion_first_note?
self == discussion.first_note
end
+ def banzai_render_context(field)
+ super.merge(suggestions_filter_enabled: supports_suggestion?)
+ end
+
private
def enqueue_diff_file_creation_job
diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb
index 1176861a827..527ee33b83b 100644
--- a/app/models/diff_viewer/base.rb
+++ b/app/models/diff_viewer/base.rb
@@ -18,7 +18,7 @@ module DiffViewer
def initialize(diff_file)
@diff_file = diff_file
- @initially_binary = diff_file.binary?
+ @initially_binary = diff_file.binary_in_repo?
end
def self.partial_path
@@ -48,7 +48,7 @@ module DiffViewer
def self.can_render_blob?(blob, verify_binary: true)
return true if blob.nil?
- return false if verify_binary && binary? != blob.binary?
+ return false if verify_binary && binary? != blob.binary_in_repo?
return true if extensions&.include?(blob.extension)
return true if file_types&.include?(blob.file_type)
@@ -70,20 +70,49 @@ module DiffViewer
end
def binary_detected_after_load?
- !@initially_binary && diff_file.binary?
+ !@initially_binary && diff_file.binary_in_repo?
end
# This method is used on the server side to check whether we can attempt to
- # render the diff_file at all. Human-readable error messages are found in the
- # `BlobHelper#diff_render_error_reason` helper.
+ # render the diff_file at all. The human-readable error message can be
+ # retrieved by #render_error_message.
def render_error
if too_large?
:too_large
end
end
+ def render_error_message
+ return unless render_error
+
+ _("This %{viewer} could not be displayed because %{reason}. You can %{options} instead.") %
+ {
+ viewer: switcher_title,
+ reason: render_error_reason,
+ options: render_error_options.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
+ }
+ end
+
def prepare!
# To be overridden by subclasses
end
+
+ private
+
+ def render_error_options
+ options = []
+
+ blob_url = Gitlab::Routing.url_helpers.project_blob_path(diff_file.repository.project,
+ File.join(diff_file.content_sha, diff_file.file_path))
+ options << ActionController::Base.helpers.link_to(_('view the blob'), blob_url)
+
+ options
+ end
+
+ def render_error_reason
+ if render_error == :too_large
+ _("it is too large")
+ end
+ end
end
end
diff --git a/app/models/diff_viewer/image.rb b/app/models/diff_viewer/image.rb
index c356c2ca50e..350bef1d42a 100644
--- a/app/models/diff_viewer/image.rb
+++ b/app/models/diff_viewer/image.rb
@@ -9,6 +9,6 @@ module DiffViewer
self.extensions = UploaderHelper::IMAGE_EXT
self.binary = true
self.switcher_icon = 'picture-o'
- self.switcher_title = 'image diff'
+ self.switcher_title = _('image diff')
end
end
diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb
index 2faa1be6567..5caefa2031c 100644
--- a/app/models/diff_viewer/rich.rb
+++ b/app/models/diff_viewer/rich.rb
@@ -7,7 +7,7 @@ module DiffViewer
included do
self.type = :rich
self.switcher_icon = 'file-text-o'
- self.switcher_title = 'rendered diff'
+ self.switcher_title = _('rendered diff')
end
end
end
diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb
index 977204e6c97..0877c9dddec 100644
--- a/app/models/diff_viewer/server_side.rb
+++ b/app/models/diff_viewer/server_side.rb
@@ -24,5 +24,17 @@ module DiffViewer
super
end
+
+ private
+
+ def render_error_reason
+ return super unless render_error == :server_side_but_stored_externally
+
+ if diff_file.external_storage == :lfs
+ _('it is stored in LFS')
+ else
+ _('it is stored externally')
+ end
+ end
end
end
diff --git a/app/models/diff_viewer/simple.rb b/app/models/diff_viewer/simple.rb
index 8d28ca5239a..929d8ad5a7e 100644
--- a/app/models/diff_viewer/simple.rb
+++ b/app/models/diff_viewer/simple.rb
@@ -7,7 +7,7 @@ module DiffViewer
included do
self.type = :simple
self.switcher_icon = 'code'
- self.switcher_title = 'source diff'
+ self.switcher_title = _('source diff')
end
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 934828946b9..cdfe3b7c023 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Environment < ActiveRecord::Base
+ include Gitlab::Utils::StrongMemoize
# Used to generate random suffixes for the slug
LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
@@ -231,7 +232,9 @@ class Environment < ActiveRecord::Base
end
def deployment_platform
- project.deployment_platform(environment: self.name)
+ strong_memoize(:deployment_platform) do
+ project.deployment_platform(environment: self.name)
+ end
end
private
diff --git a/app/models/event.rb b/app/models/event.rb
index 2ceef412af5..6a35bca72c5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -114,19 +114,6 @@ class Event < ActiveRecord::Base
end
end
- # Remove this method when removing Gitlab.rails5? code.
- def subclass_from_attributes(attrs)
- return super if Gitlab.rails5?
-
- # Without this Rails will keep calling this method on the returned class,
- # resulting in an infinite loop.
- return unless self == Event
-
- action = attrs.with_indifferent_access[inheritance_column].to_i
-
- PushEvent if action == PUSHED
- end
-
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 085ffd16c6a..4e82f3fed27 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -3,69 +3,78 @@
class GlobalMilestone
include Milestoneish
- EPOCH = DateTime.parse('1970-01-01')
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
- attr_accessor :title, :milestones
+ attr_reader :milestone
alias_attribute :name, :title
+ delegate :title, :state, :due_date, :start_date, :participants, :project, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, :milestoneish_id, to: :milestone
+
+ def to_hash
+ {
+ name: title,
+ title: title,
+ group_name: group&.full_name,
+ project_name: project&.full_name
+ }
+ end
+
def for_display
- @first_milestone
+ @milestone
end
def self.build_collection(projects, params)
- params =
- { project_ids: projects.map(&:id), state: params[:state] }
-
- child_milestones = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
-
- milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
- milestones_relation = Milestone.where(id: grouped.map(&:id))
- new(title, milestones_relation)
- end
+ items = Milestone.of_projects(projects)
+ .reorder_by_due_date_asc
+ .order_by_name_asc
- milestones.sort_by { |milestone| milestone.due_date || EPOCH }
+ Milestone.filter_by_state(items, params[:state]).map { |m| new(m) }
end
+ # necessary for legacy milestones
def self.build(projects, title)
- child_milestones = Milestone.of_projects(projects).where(title: title)
- return if child_milestones.blank?
+ milestones = Milestone.of_projects(projects).where(title: title)
+ return if milestones.blank?
- new(title, child_milestones)
+ new(milestones.first)
end
- def self.count_by_state(milestones_by_state_and_title, state)
- milestones_by_state_and_title.count do |(milestone_state, _), _|
- milestone_state == state
+ def self.states_count(projects, group = nil)
+ legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
+ group_milestones_count = group_milestones_states_count(group)
+
+ legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
+ legacy_group_milestones_count + group_milestones_count
end
end
- private_class_method :count_by_state
- def initialize(title, milestones)
- @title = title
- @name = title
- @milestones = milestones
- @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
- end
+ def self.group_milestones_states_count(group)
+ return STATE_COUNT_HASH unless group
- def milestoneish_ids
- milestones.select(:id)
- end
+ counts_by_state = Milestone.of_groups(group).count_by_state
- def safe_title
- @title.to_slug.normalize.to_s
+ {
+ opened: counts_by_state['active'] || 0,
+ closed: counts_by_state['closed'] || 0,
+ all: counts_by_state.values.sum
+ }
end
- def projects
- @projects ||= Project.for_milestones(milestoneish_ids)
- end
+ def self.legacy_group_milestone_states_count(projects)
+ return STATE_COUNT_HASH unless projects
- def state
- milestones.each do |milestone|
- return 'active' if milestone.state != 'closed'
- end
+ # We need to reorder(nil) on the projects, because the controller passes them in sorted.
+ relation = Milestone.of_projects(projects.reorder(nil)).count_by_state
- 'closed'
+ {
+ opened: relation['active'] || 0,
+ closed: relation['closed'] || 0,
+ all: relation.values.sum
+ }
+ end
+
+ def initialize(milestone)
+ @milestone = milestone
end
def active?
@@ -77,37 +86,14 @@ class GlobalMilestone
end
def issues
- @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignees, :labels)
+ @issues ||= Issue.of_milestones(milestone).includes(:project, :assignees, :labels)
end
def merge_requests
- @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels)
- end
-
- def participants
- @participants ||= milestones.map(&:participants).flatten.uniq
+ @merge_requests ||= MergeRequest.of_milestones(milestone).includes(:target_project, :assignee, :labels)
end
def labels
- @labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
- .sort_by!(&:title)
- end
-
- def due_date
- return @due_date if defined?(@due_date)
-
- @due_date =
- if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
- @milestones.first.due_date
- end
- end
-
- def start_date
- return @start_date if defined?(@start_date)
-
- @start_date =
- if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
- @milestones.first.start_date
- end
+ @labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 233747cc2c2..edac2444c4d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -10,6 +10,7 @@ class Group < Namespace
include Referable
include SelectForProjectAuthorization
include LoadedInGroupList
+ include Descendant
include GroupDescendant
include TokenAuthenticatable
include WithUploads
@@ -63,10 +64,6 @@ class Group < Namespace
after_update :path_changed_hook, if: :path_changed?
class << self
- def supports_nested_groups?
- Gitlab::Database.postgresql?
- end
-
def sort_by_attribute(method)
if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 9dfaebacc83..a58537de319 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -1,18 +1,35 @@
# frozen_string_literal: true
# Group Milestones are milestones that can be shared among many projects within the same group
class GroupMilestone < GlobalMilestone
- attr_accessor :group
+ attr_reader :group, :milestones
def self.build_collection(group, projects, params)
- super(projects, params).each do |milestone|
- milestone.group = group
+ params =
+ { state: params[:state] }
+
+ project_milestones = Milestone.of_projects(projects)
+ child_milestones = Milestone.filter_by_state(project_milestones, params[:state])
+ grouped_milestones = child_milestones.group_by(&:title)
+
+ grouped_milestones.map do |title, grouped|
+ new(title, grouped, group)
end
end
def self.build(group, projects, title)
- super(projects, title).tap do |milestone|
- milestone&.group = group
- end
+ child_milestones = Milestone.of_projects(projects).where(title: title)
+ return if child_milestones.blank?
+
+ new(title, child_milestones, group)
+ end
+
+ def initialize(title, milestones, group)
+ @milestones = milestones
+ @group = group
+ end
+
+ def milestone
+ @milestone ||= milestones.find { |m| m.description.present? } || milestones.first
end
def issues_finder_params
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a13cac73d04..6092c56b925 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -48,8 +48,8 @@ class MergeRequest < ActiveRecord::Base
# is the inverse of MergeRequest#merge_request_diff, which means it may not be
# the latest diff, because we could have loaded any diff from this particular
# MR. If we haven't already loaded a diff, then it's fine to load the latest.
- def merge_request_diff(*args)
- fallback = latest_merge_request_diff if args.empty? && !association(:merge_request_diff).loaded?
+ def merge_request_diff
+ fallback = latest_merge_request_diff unless association(:merge_request_diff).loaded?
fallback || super
end
@@ -105,7 +105,9 @@ class MergeRequest < ActiveRecord::Base
before_transition any => :opened do |merge_request|
merge_request.merge_jid = nil
+ end
+ after_transition any => :opened do |merge_request|
merge_request.run_after_commit do
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
@@ -363,6 +365,10 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def supports_suggestion?
+ true
+ end
+
# Calls `MergeWorker` to proceed with the merge process and
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
@@ -404,6 +410,28 @@ class MergeRequest < ActiveRecord::Base
merge_request_diffs.where.not(id: merge_request_diff.id)
end
+ def preloads_discussion_diff_highlighting?
+ true
+ end
+
+ def preload_discussions_diff_highlight
+ preloadable_files = note_diff_files.for_commit_or_unresolved
+
+ discussions_diffs.load_highlight(preloadable_files.pluck(:id))
+ end
+
+ def discussions_diffs
+ strong_memoize(:discussions_diffs) do
+ Gitlab::DiscussionsDiff::FileCollection.new(note_diff_files.to_a)
+ end
+ end
+
+ def note_diff_files
+ NoteDiffFile
+ .where(diff_note: discussion_notes)
+ .includes(diff_note: :project)
+ end
+
def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here.
@@ -615,10 +643,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- def reload_merge_request_diff
- merge_request_diff(true)
- end
-
def viewable_diffs
@viewable_diffs ||= merge_request_diffs.viewable.to_a
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 3cc8e2c44bb..f55c39d9912 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -94,6 +94,10 @@ class Milestone < ActiveRecord::Base
end
end
+ def count_by_state
+ reorder(nil).group(:state).count
+ end
+
def predefined?(milestone)
milestone == Any ||
milestone == None ||
@@ -212,10 +216,10 @@ class Milestone < ActiveRecord::Base
end
def reference_link_text(from = nil)
- self.title
+ self.class.reference_prefix + self.title
end
- def milestoneish_ids
+ def milestoneish_id
id
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3c9b1d32a53..a0bebc5e9a2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -175,16 +175,16 @@ class Namespace < ActiveRecord::Base
# Returns all ancestors, self, and descendants of the current namespace.
def self_and_hierarchy
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
- .all_groups
+ .all_objects
end
# Returns all the ancestors of the current namespaces.
def ancestors
return self.class.none unless parent_id
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(self.class.where(id: parent_id))
.base_and_ancestors
end
@@ -192,27 +192,27 @@ class Namespace < ActiveRecord::Base
# returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
- Gitlab::GroupHierarchy.new(self.class.where(id: id))
+ Gitlab::ObjectHierarchy.new(self.class.where(id: id))
.ancestors(upto: top, hierarchy_order: hierarchy_order)
end
def self_and_ancestors
return self.class.where(id: id) unless parent_id
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
.base_and_ancestors
end
# Returns all the descendants of the current namespace.
def descendants
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(self.class.where(parent_id: id))
.base_and_descendants
end
def self_and_descendants
- Gitlab::GroupHierarchy
+ Gitlab::ObjectHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
@@ -293,7 +293,7 @@ class Namespace < ActiveRecord::Base
end
def force_share_with_group_lock_on_descendants
- return unless Group.supports_nested_groups?
+ return unless Group.supports_nested_objects?
# We can't use `descendants.update_all` since Rails will throw away the WITH
# RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
@@ -306,6 +306,7 @@ class Namespace < ActiveRecord::Base
def write_projects_repository_config
all_projects.find_each do |project|
project.write_repository_config
+ project.track_project_repository
end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 17c7d97fa0a..becf14e9785 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -69,6 +69,12 @@ class Note < ActiveRecord::Base
belongs_to :last_edited_by, class_name: 'User'
has_many :todos
+
+ # The delete_all definition is required here in order
+ # to generate the correct DELETE sql for
+ # suggestions.delete_all calls
+ has_many :suggestions, -> { order(:relative_order) },
+ inverse_of: :note, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
@@ -110,7 +116,7 @@ class Note < ActiveRecord::Base
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
includes(:project, { author: :status }, :updated_by, :resolved_by, :award_emoji,
- :system_note_metadata, :note_diff_file)
+ :system_note_metadata, :note_diff_file, :suggestions)
end
scope :with_notes_filter, -> (notes_filter) do
@@ -226,6 +232,10 @@ class Note < ActiveRecord::Base
Gitlab::HookData::NoteBuilder.new(self).build
end
+ def supports_suggestion?
+ false
+ end
+
def for_commit?
noteable_type == "Commit"
end
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
index 27aef7adc48..e369122003e 100644
--- a/app/models/note_diff_file.rb
+++ b/app/models/note_diff_file.rb
@@ -3,7 +3,22 @@
class NoteDiffFile < ActiveRecord::Base
include DiffFile
+ scope :for_commit_or_unresolved, -> do
+ joins(:diff_note).where("resolved_at IS NULL OR noteable_type = 'Commit'")
+ end
+
+ delegate :original_position, :project, to: :diff_note
+
belongs_to :diff_note, inverse_of: :note_diff_file
validates :diff_note, presence: true
+
+ def raw_diff_file
+ raw_diff = Gitlab::Git::Diff.new(to_hash)
+
+ Gitlab::Diff::File.new(raw_diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs,
+ unique_identifier: id)
+ end
end
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 47da0209c2f..ad6a008dee8 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -18,6 +18,7 @@ class PoolRepository < ActiveRecord::Base
state :scheduled
state :ready
state :failed
+ state :obsolete
event :schedule do
transition none: :scheduled
@@ -31,6 +32,10 @@ class PoolRepository < ActiveRecord::Base
transition all => :failed
end
+ event :mark_obsolete do
+ transition all => :obsolete
+ end
+
state all - [:ready] do
def joinable?
false
@@ -54,6 +59,12 @@ class PoolRepository < ActiveRecord::Base
::ObjectPool::ScheduleJoinWorker.perform_async(pool.id)
end
end
+
+ after_transition any => :obsolete do |pool, _|
+ pool.run_after_commit do
+ ::ObjectPool::DestroyWorker.perform_async(pool.id)
+ end
+ end
end
def create_object_pool
@@ -71,10 +82,10 @@ class PoolRepository < ActiveRecord::Base
end
# This RPC can cause data loss, as not all objects are present the local repository
- # No execution path yet, will be added through:
- # https://gitlab.com/gitlab-org/gitaly/issues/1415
- def delete_repository_alternate(repository)
+ def unlink_repository(repository)
object_pool.unlink_repository(repository.raw)
+
+ mark_obsolete unless member_projects.where.not(id: repository.project.id).exists?
end
def object_pool
diff --git a/app/models/project.rb b/app/models/project.rb
index 67262ecce85..58b10662ff0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -256,7 +256,7 @@ class Project < ActiveRecord::Base
# other pipelines, like webide ones, that we won't retrieve
# if we use this relation.
has_many :ci_pipelines,
- -> { Feature.enabled?(:pipeline_ci_sources_only, default_enabled: true) ? ci_sources : all },
+ -> { ci_sources },
class_name: 'Ci::Pipeline',
inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
@@ -324,15 +324,14 @@ class Project < ActiveRecord::Base
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
- validates :import_url, url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
- ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
- allow_localhost: false,
- enforce_user: true }, if: [:external_import?, :import_url_changed?]
+ validates :import_url, public_url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
+ ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
+ enforce_user: true }, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
- validate :visibility_level_allowed_by_group
- validate :visibility_level_allowed_as_fork
+ validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
+ validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
validate :check_wiki_path_conflict
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
@@ -570,7 +569,7 @@ class Project < ActiveRecord::Base
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
- Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
+ Gitlab::ObjectHierarchy.new(Group.where(id: namespace_id))
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
@@ -913,11 +912,16 @@ class Project < ActiveRecord::Base
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
+ # check since this can come from a request parameter
+ return unless %w(issue merge_request).include?(address_type)
+
author.ensure_incoming_email_token!
- suffix = address_type == 'merge_request' ? '+merge-request' : ''
- Gitlab::IncomingEmail.reply_address(
- "#{full_path}#{suffix}+#{author.incoming_email_token}")
+ suffix = address_type.dasherize
+
+ # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue@localhost.com
+ # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-merge-request@localhost.com
+ Gitlab::IncomingEmail.reply_address("#{full_path_slug}-#{project_id}-#{author.incoming_email_token}-#{suffix}")
end
def build_commit_note(commit)
@@ -1244,10 +1248,8 @@ class Project < ActiveRecord::Base
end
def track_project_repository
- return unless hashed_storage?(:repository)
-
- project_repo = project_repository || build_project_repository
- project_repo.update!(shard_name: repository_storage, disk_path: disk_path)
+ repository = project_repository || build_project_repository
+ repository.update!(shard_name: repository_storage, disk_path: disk_path)
end
def create_repository(force: false)
@@ -1736,10 +1738,21 @@ class Project < ActiveRecord::Base
end
def protected_for?(ref)
- if repository.branch_exists?(ref)
- ProtectedBranch.protected?(self, ref)
- elsif repository.tag_exists?(ref)
- ProtectedTag.protected?(self, ref)
+ raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
+
+ resolved_ref = repository.expand_ref(ref) || ref
+ return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)
+
+ ref_name = if resolved_ref == ref
+ Gitlab::Git.ref_name(resolved_ref)
+ else
+ ref
+ end
+
+ if Gitlab::Git.branch_ref?(resolved_ref)
+ ProtectedBranch.protected?(self, ref_name)
+ elsif Gitlab::Git.tag_ref?(resolved_ref)
+ ProtectedTag.protected?(self, ref_name)
end
end
@@ -1920,23 +1933,15 @@ class Project < ActiveRecord::Base
.where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1)
.select(1)
- source_of_merge_requests.opened
- .where(allow_collaboration: true)
- .where('EXISTS (?)', developer_access_exists)
+ merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
end
- def branch_allows_collaboration?(user, branch_name)
- return false unless user
-
- cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
-
- memoized_results = strong_memoize(:branch_allows_collaboration) do
- Hash.new do |result, cache_key|
- result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name)
- end
- end
+ def any_branch_allows_collaboration?(user)
+ fetch_branch_allows_collaboration(user)
+ end
- memoized_results[cache_key]
+ def branch_allows_collaboration?(user, branch_name)
+ fetch_branch_allows_collaboration(user, branch_name)
end
def licensed_features
@@ -2004,8 +2009,18 @@ class Project < ActiveRecord::Base
Feature.enabled?(:object_pools, self)
end
+ def leave_pool_repository
+ pool_repository&.unlink_repository(repository)
+ end
+
private
+ def merge_requests_allowing_collaboration(source_branch = nil)
+ relation = source_of_merge_requests.opened.where(allow_collaboration: true)
+ relation = relation.where(source_branch: source_branch) if source_branch
+ relation
+ end
+
def create_new_pool_repository
pool = begin
create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
@@ -2130,26 +2145,19 @@ class Project < ActiveRecord::Base
raise ex
end
- def fetch_branch_allows_collaboration?(user, branch_name)
- check_access = -> do
- next false if empty_repo?
+ def fetch_branch_allows_collaboration(user, branch_name = nil)
+ return false unless user
- merge_requests = source_of_merge_requests.opened
- .where(allow_collaboration: true)
+ Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
+ next false if empty_repo?
# Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322
Gitlab::GitalyClient.allow_n_plus_1_calls do
- if branch_name
- merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user)
- else
- merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) }
+ merge_requests_allowing_collaboration(branch_name).any? do |merge_request|
+ merge_request.can_be_merged_by?(user)
end
end
end
-
- Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
- check_access.call
- end
end
def services_templates
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index b801fd84a07..f69edd60003 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -53,7 +53,7 @@ class KubernetesService < DeploymentService
end
def description
- 'Kubernetes / Openshift integration'
+ 'Kubernetes / OpenShift integration'
end
def help
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index ce2db9cb44c..5594594a48d 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -5,11 +5,12 @@ class PrometheusMetric < ActiveRecord::Base
enum group: {
# built-in groups
- nginx_ingress: -1,
+ nginx_ingress_vts: -1,
ha_proxy: -2,
aws_elb: -3,
nginx: -4,
kubernetes: -5,
+ nginx_ingress: -6,
# custom/user groups
business: 0,
@@ -17,6 +18,54 @@ class PrometheusMetric < ActiveRecord::Base
system: 2
}
+ GROUP_DETAILS = {
+ # built-in groups
+ nginx_ingress_vts: {
+ group_title: _('Response metrics (NGINX Ingress VTS)'),
+ required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ priority: 10
+ }.freeze,
+ nginx_ingress: {
+ group_title: _('Response metrics (NGINX Ingress)'),
+ required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
+ priority: 10
+ }.freeze,
+ ha_proxy: {
+ group_title: _('Response metrics (HA Proxy)'),
+ required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ priority: 10
+ }.freeze,
+ aws_elb: {
+ group_title: _('Response metrics (AWS ELB)'),
+ required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ priority: 10
+ }.freeze,
+ nginx: {
+ group_title: _('Response metrics (NGINX)'),
+ required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
+ priority: 10
+ }.freeze,
+ kubernetes: {
+ group_title: _('System metrics (Kubernetes)'),
+ required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ priority: 5
+ }.freeze,
+
+ # custom/user groups
+ business: {
+ group_title: _('Business metrics (Custom)'),
+ priority: 0
+ }.freeze,
+ response: {
+ group_title: _('Response metrics (Custom)'),
+ priority: -5
+ }.freeze,
+ system: {
+ group_title: _('System metrics (Custom)'),
+ priority: -10
+ }.freeze
+ }.freeze
+
validates :title, presence: true
validates :query, presence: true
validates :group, presence: true
@@ -28,34 +77,16 @@ class PrometheusMetric < ActiveRecord::Base
scope :common, -> { where(common: true) }
- GROUP_TITLES = {
- # built-in groups
- nginx_ingress: _('Response metrics (NGINX Ingress)'),
- ha_proxy: _('Response metrics (HA Proxy)'),
- aws_elb: _('Response metrics (AWS ELB)'),
- nginx: _('Response metrics (NGINX)'),
- kubernetes: _('System metrics (Kubernetes)'),
-
- # custom/user groups
- business: _('Business metrics (Custom)'),
- response: _('Response metrics (Custom)'),
- system: _('System metrics (Custom)')
- }.freeze
-
- REQUIRED_METRICS = {
- nginx_ingress: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
- ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
- aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
- nginx: %w(nginx_server_requests nginx_server_requestMsec),
- kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
- }.freeze
+ def priority
+ group_details(group).fetch(:priority)
+ end
def group_title
- GROUP_TITLES[group.to_sym]
+ group_details(group).fetch(:group_title)
end
def required_metrics
- REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s)
+ group_details(group).fetch(:required_metrics, []).map(&:to_s)
end
def to_query_metric
@@ -86,4 +117,10 @@ class PrometheusMetric < ActiveRecord::Base
}]
end
end
+
+ private
+
+ def group_details(group)
+ GROUP_DETAILS.fetch(group.to_sym)
+ end
end
diff --git a/app/models/release.rb b/app/models/release.rb
index cba80ad30ca..df3dfe1cf2f 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -2,10 +2,39 @@
class Release < ActiveRecord::Base
include CacheMarkdownField
+ include Gitlab::Utils::StrongMemoize
cache_markdown_field :description
belongs_to :project
+ # releases prior to 11.7 have no author
+ belongs_to :author, class_name: 'User'
validates :description, :project, :tag, presence: true
+
+ scope :sorted, -> { order(created_at: :desc) }
+
+ delegate :repository, to: :project
+
+ def commit
+ strong_memoize(:commit) do
+ repository.commit(actual_sha)
+ end
+ end
+
+ def tag_missing?
+ actual_tag.nil?
+ end
+
+ private
+
+ def actual_sha
+ sha || actual_tag&.dereferenced_target
+ end
+
+ def actual_tag
+ strong_memoize(:actual_tag) do
+ repository.find_tag(tag)
+ end
+ end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 5a6895aefab..a3fa67c72bf 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -17,7 +17,7 @@ class RemoteMirror < ActiveRecord::Base
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
+ validates :url, presence: true, public_url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
before_save :set_new_remote_name, if: :mirror_url_changed?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 015a179f374..b19ae2e0e6a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -25,6 +25,7 @@ class Repository
delegate :bundle_to_disk, to: :raw_repository
CreateTreeError = Class.new(StandardError)
+ AmbiguousRefError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
@@ -181,6 +182,18 @@ class Repository
tags.find { |tag| tag.name == name }
end
+ def ambiguous_ref?(ref)
+ tag_exists?(ref) && branch_exists?(ref)
+ end
+
+ def expand_ref(ref)
+ if tag_exists?(ref)
+ Gitlab::Git::TAG_REF_PREFIX + ref
+ elsif branch_exists?(ref)
+ Gitlab::Git::BRANCH_REF_PREFIX + ref
+ end
+ end
+
def add_branch(user, branch_name, ref)
branch = raw_repository.add_branch(branch_name, user: user, target: ref)
diff --git a/app/models/service.rb b/app/models/service.rb
index 5b8bf6e7cf0..9dcb0aab0a3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -210,11 +210,7 @@ class Service < ActiveRecord::Base
class_eval %{
def #{arg}?
# '!!' is used because nil or empty string is converted to nil
- if Gitlab.rails5?
- !!ActiveRecord::Type::Boolean.new.cast(#{arg})
- else
- !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg})
- end
+ !!ActiveRecord::Type::Boolean.new.cast(#{arg})
end
}
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 11856b55902..f9b23bbbf6c 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -175,6 +175,12 @@ class Snippet < ActiveRecord::Base
:visibility_level
end
+ def embeddable?
+ ability = project_id? ? :read_project_snippet : :read_personal_snippet
+
+ Ability.allowed?(nil, ability, self)
+ end
+
def notes_with_associations
notes.includes(:author)
end
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
new file mode 100644
index 00000000000..c76b8e71507
--- /dev/null
+++ b/app/models/suggestion.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+class Suggestion < ApplicationRecord
+ belongs_to :note, inverse_of: :suggestions
+ validates :note, presence: true
+ validates :commit_id, presence: true, if: :applied?
+
+ delegate :original_position, :position, :diff_file,
+ :noteable, to: :note
+
+ def project
+ noteable.source_project
+ end
+
+ def branch
+ noteable.source_branch
+ end
+
+ # For now, suggestions only serve as a way to send patches that
+ # will change a single line (being able to apply multiple in the same place),
+ # which explains `from_line` and `to_line` being the same line.
+ # We'll iterate on that in https://gitlab.com/gitlab-org/gitlab-ce/issues/53310
+ # when allowing multi-line suggestions.
+ def from_line
+ position.new_line
+ end
+ alias_method :to_line, :from_line
+
+ def from_original_line
+ original_position.new_line
+ end
+ alias_method :to_original_line, :from_original_line
+
+ # `from_line_index` and `to_line_index` represents diff/blob line numbers in
+ # index-like way (N-1).
+ def from_line_index
+ from_line - 1
+ end
+ alias_method :to_line_index, :from_line_index
+
+ def appliable?
+ return false unless note.supports_suggestion?
+
+ !applied? &&
+ noteable.opened? &&
+ different_content? &&
+ note.active?
+ end
+
+ private
+
+ def different_content?
+ from_content != to_content
+ end
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 7b64615f699..d9b86d941b6 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -4,6 +4,11 @@ class Todo < ActiveRecord::Base
include Sortable
include FromUnion
+ # Time to wait for todos being removed when not visible for user anymore.
+ # Prevents TODOs being removed by mistake, for example, removing access from a user
+ # and giving it back again.
+ WAIT_FOR_DELETE = 1.hour
+
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
diff --git a/app/models/user.rb b/app/models/user.rb
index dbd754dd25a..26fd2d903a1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -130,6 +130,7 @@ class User < ActiveRecord::Base
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :events, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
@@ -708,13 +709,13 @@ class User < ActiveRecord::Base
# Returns the groups a user is a member of, either directly or through a parent group
def membership_groups
- Gitlab::GroupHierarchy.new(groups).base_and_descendants
+ Gitlab::ObjectHierarchy.new(groups).base_and_descendants
end
# Returns a relation of groups the user has access to, including their parent
# and child groups (recursively).
def all_expanded_groups
- Gitlab::GroupHierarchy.new(groups).all_groups
+ Gitlab::ObjectHierarchy.new(groups).all_objects
end
def expanded_groups_requiring_two_factor_authentication
@@ -1152,7 +1153,7 @@ class User < ActiveRecord::Base
end
def manageable_groups
- Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants
+ Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
end
def namespaces
@@ -1421,6 +1422,10 @@ class User < ActiveRecord::Base
todos.where(id: ids)
end
+ def pending_todo_for(target)
+ todos.find_by(target: target, state: :pending)
+ end
+
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
diff --git a/app/policies/concerns/clusterable_actions.rb b/app/policies/concerns/clusterable_actions.rb
new file mode 100644
index 00000000000..08ddd742ea9
--- /dev/null
+++ b/app/policies/concerns/clusterable_actions.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ClusterableActions
+ private
+
+ # Overridden on EE module
+ def multiple_clusters_available?
+ false
+ end
+
+ def clusterable_has_clusters?
+ !subject.clusters.empty?
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 6b4e56ef5e4..c25766a5af8 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GroupPolicy < BasePolicy
+ include ClusterableActions
+
desc "Group is public"
with_options scope: :subject, score: 0
condition(:public_group) { @subject.public? }
@@ -16,7 +18,7 @@ class GroupPolicy < BasePolicy
condition(:maintainer) { access_level >= GroupMember::MAINTAINER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
- condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
+ condition(:nested_groups_supported, scope: :global) { Group.supports_nested_objects? }
condition(:has_parent, scope: :subject) { @subject.has_parent? }
condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
@@ -27,6 +29,9 @@ class GroupPolicy < BasePolicy
GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end
+ condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
+ condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
@@ -40,11 +45,12 @@ class GroupPolicy < BasePolicy
rule { guest }.policy do
enable :read_group
+ enable :read_list
enable :upload_file
enable :read_label
end
- rule { admin } .enable :read_group
+ rule { admin }.enable :read_group
rule { has_projects }.policy do
enable :read_group
@@ -66,6 +72,7 @@ class GroupPolicy < BasePolicy
enable :admin_pipeline
enable :admin_build
enable :read_cluster
+ enable :add_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
@@ -105,6 +112,8 @@ class GroupPolicy < BasePolicy
rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
+ rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+
def access_level
return GroupMember::NO_ACCESS if @user.nil?
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 6d8b575102e..ecb2797d1d9 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -11,7 +11,7 @@ class IssuablePolicy < BasePolicy
@user && @subject.assignee_or_author?(@user)
end
- rule { assignee_or_author }.policy do
+ rule { can?(:guest_access) & assignee_or_author }.policy do
enable :read_issue
enable :update_issue
enable :reopen_issue
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 1c082945299..3146f26bed5 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,6 +2,7 @@
class ProjectPolicy < BasePolicy
extend ClassMethods
+ include ClusterableActions
READONLY_FEATURES_WHEN_ARCHIVED = %i[
issue
@@ -22,6 +23,7 @@ class ProjectPolicy < BasePolicy
container_image
pages
cluster
+ release
].freeze
desc "User is a project owner"
@@ -103,6 +105,9 @@ class ProjectPolicy < BasePolicy
@subject.feature_available?(:merge_requests, @user)
end
+ condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
+ condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+
features = %w[
merge_requests
issues
@@ -169,6 +174,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :award_emoji
enable :read_pages_content
+ enable :read_release
end
# These abilities are not allowed to admins that are not members of the project,
@@ -235,6 +241,8 @@ class ProjectPolicy < BasePolicy
enable :update_container_image
enable :create_environment
enable :create_deployment
+ enable :create_release
+ enable :update_release
end
rule { can?(:maintainer_access) }.policy do
@@ -257,10 +265,12 @@ class ProjectPolicy < BasePolicy
enable :read_pages
enable :update_pages
enable :read_cluster
+ enable :add_cluster
enable :create_cluster
enable :update_cluster
enable :admin_cluster
enable :create_environment_terminal
+ enable :destroy_release
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
@@ -320,6 +330,7 @@ class ProjectPolicy < BasePolicy
prevent :download_code
prevent :fork_project
prevent :read_commit_status
+ prevent(*create_read_update_admin_destroy(:release))
end
rule { container_registry_disabled }.policy do
@@ -349,6 +360,7 @@ class ProjectPolicy < BasePolicy
enable :read_commit_status
enable :read_container_image
enable :download_code
+ enable :read_release
enable :download_wiki_code
enable :read_cycle_analytics
enable :read_pages_content
@@ -381,6 +393,8 @@ class ProjectPolicy < BasePolicy
(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
+
private
def team_member?
diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb
new file mode 100644
index 00000000000..d7f9e5d7445
--- /dev/null
+++ b/app/policies/release_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ReleasePolicy < BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/policies/suggestion_policy.rb b/app/policies/suggestion_policy.rb
new file mode 100644
index 00000000000..301b7d965f5
--- /dev/null
+++ b/app/policies/suggestion_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class SuggestionPolicy < BasePolicy
+ delegate { @subject.project }
+
+ condition(:can_push_to_branch) do
+ Gitlab::UserAccess.new(@user, project: @subject.project).can_push_to_branch?(@subject.branch)
+ end
+
+ rule { can_push_to_branch }.enable :apply_suggestion
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 9cc137aa3bd..d94d9118eee 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -12,6 +12,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
.fabricate!
end
+ def can_add_cluster?
+ can?(current_user, :add_cluster, clusterable)
+ end
+
def can_create_cluster?
can?(current_user, :create_cluster, clusterable)
end
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 7e6eccb648c..7a5b68f9a4b 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -2,8 +2,22 @@
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ include ActionView::Helpers::SanitizeHelper
+ include ActionView::Helpers::UrlHelper
+ include IconsHelper
+
presents :cluster
+ # We do not want to show the group path for clusters belonging to the
+ # clusterable, only for the ancestor clusters.
+ def item_link(clusterable_presenter)
+ if cluster.group_type? && clusterable != clusterable_presenter.subject
+ contracted_group_name(cluster.group) + ' / ' + link_to_cluster
+ else
+ link_to_cluster
+ end
+ end
+
def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
@@ -12,6 +26,18 @@ module Clusters
can?(current_user, :update_cluster, cluster) && created?
end
+ def can_read_cluster?
+ can?(current_user, :read_cluster, cluster)
+ end
+
+ def cluster_type_description
+ if cluster.project_type?
+ s_("ClusterIntegration|Project cluster")
+ elsif cluster.group_type?
+ s_("ClusterIntegration|Group cluster")
+ end
+ end
+
def show_path
if cluster.project_type?
project_cluster_path(project, cluster)
@@ -21,5 +47,29 @@ module Clusters
raise NotImplementedError
end
end
+
+ private
+
+ def clusterable
+ if cluster.group_type?
+ cluster.group
+ elsif cluster.project_type?
+ cluster.project
+ end
+ end
+
+ def contracted_group_name(group)
+ sanitize(group.full_name)
+ .sub(%r{\/.*\/}, "/ #{contracted_icon} /")
+ .html_safe
+ end
+
+ def contracted_icon
+ sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
+ end
+
+ def link_to_cluster
+ link_to_if(can_read_cluster?, cluster.name, show_path)
+ end
end
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index f0881829efd..b0aaec3326d 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -5,6 +5,7 @@ class DiffFileEntity < DiffFileBaseEntity
include IconsHelper
expose :too_large?, as: :too_large
+ expose :empty?, as: :empty
expose :added_lines
expose :removed_lines
diff --git a/app/serializers/diff_line_entity.rb b/app/serializers/diff_line_entity.rb
index 942714b7787..bfef6d3bde8 100644
--- a/app/serializers/diff_line_entity.rb
+++ b/app/serializers/diff_line_entity.rb
@@ -11,4 +11,6 @@ class DiffLineEntity < Grape::Entity
expose :rich_text do |line|
ERB::Util.html_escape(line.rich_text || line.text)
end
+
+ expose :suggestible?, as: :can_receive_suggestion
end
diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb
index 27fba03cb3f..587fa2347fd 100644
--- a/app/serializers/diff_viewer_entity.rb
+++ b/app/serializers/diff_viewer_entity.rb
@@ -4,4 +4,7 @@ class DiffViewerEntity < Grape::Entity
# Partial name refers directly to a Rails feature, let's avoid
# using this on the frontend.
expose :partial_name, as: :name
+ expose :error do |diff_viewer|
+ diff_viewer.render_error_message
+ end
end
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
index cc0c2abf863..f515abe5917 100644
--- a/app/serializers/entity_date_helper.rb
+++ b/app/serializers/entity_date_helper.rb
@@ -44,14 +44,14 @@ module EntityDateHelper
# It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed"
- def remaining_days_in_words(entity)
- if entity.try(:expired?)
+ def remaining_days_in_words(due_date, start_date = nil)
+ if due_date&.past?
content_tag(:strong, 'Past due')
- elsif entity.try(:upcoming?)
+ elsif start_date&.future?
content_tag(:strong, 'Upcoming')
- elsif entity.due_date
- is_upcoming = (entity.due_date - Date.today).to_i > 0
- time_ago = time_ago_in_words(entity.due_date)
+ elsif due_date
+ is_upcoming = (due_date - Date.today).to_i > 0
+ time_ago = time_ago_in_words(due_date)
# https://gitlab.com/gitlab-org/gitlab-ce/issues/49440
#
@@ -63,8 +63,8 @@ module EntityDateHelper
remaining_or_ago = is_upcoming ? _("remaining") : _("ago")
"#{content} #{remaining_or_ago}".html_safe
- elsif entity.start_date && entity.start_date.past?
- days = entity.elapsed_days
+ elsif start_date&.past?
+ days = (Date.today - start_date).to_i
"#{content_tag(:strong, days)} #{'day'.pluralize(days)} elapsed".html_safe
end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 07a13c33b89..4a7d13915dd 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -23,6 +23,10 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
end
+ expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
+ cluster.cluster_type
+ end
+
expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment|
terminal_project_environment_path(environment.project, environment)
end
@@ -48,4 +52,16 @@ class EnvironmentEntity < Grape::Entity
def can_access_terminal?
can?(request.current_user, :create_environment_terminal, environment)
end
+
+ def cluster_platform_kubernetes?
+ deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
+ end
+
+ def deployment_platform
+ environment.deployment_platform
+ end
+
+ def cluster
+ deployment_platform.cluster
+ end
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
new file mode 100644
index 00000000000..61de3c93337
--- /dev/null
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+class IssuableSidebarBasicEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :type do |issuable|
+ issuable.to_ability_name
+ end
+ expose :author_id
+ expose :project_id do |issuable|
+ issuable.project.id
+ end
+ expose :discussion_locked
+ expose :reference do |issuable|
+ issuable.to_reference(issuable.project, full: true)
+ end
+
+ expose :milestone, using: ::API::Entities::Milestone
+ expose :labels, using: LabelEntity
+
+ expose :current_user, if: lambda { |_issuable| current_user } do
+ expose :current_user, merge: true, using: API::Entities::UserBasic
+
+ expose :todo, using: IssuableSidebarTodoEntity do |issuable|
+ current_user.pending_todo_for(issuable)
+ end
+
+ expose :can_edit do |issuable|
+ can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ end
+
+ expose :can_move do |issuable|
+ issuable.can_move?(current_user)
+ end
+
+ expose :can_admin_label do |issuable|
+ can?(current_user, :admin_label, issuable.project)
+ end
+ end
+
+ expose :issuable_json_path do |issuable|
+ if issuable.is_a?(MergeRequest)
+ project_merge_request_path(issuable.project, issuable.iid, :json)
+ else
+ project_issue_path(issuable.project, issuable.iid, :json)
+ end
+ end
+
+ expose :namespace_path do |issuable|
+ issuable.project.namespace.full_path
+ end
+
+ expose :project_path do |issuable|
+ issuable.project.path
+ end
+
+ expose :project_full_path do |issuable|
+ issuable.project.full_path
+ end
+
+ expose :project_issuables_path do |issuable|
+ project = issuable.project
+ namespace = project.namespace
+
+ if issuable.is_a?(MergeRequest)
+ namespace_project_merge_requests_path(namespace, project)
+ else
+ namespace_project_issues_path(namespace, project)
+ end
+ end
+
+ expose :create_todo_path do |issuable|
+ project_todos_path(issuable.project)
+ end
+
+ expose :project_milestones_path do |issuable|
+ project_milestones_path(issuable.project, :json)
+ end
+
+ expose :project_labels_path do |issuable|
+ project_labels_path(issuable.project, :json, include_ancestor_groups: true)
+ end
+
+ expose :toggle_subscription_path do |issuable|
+ toggle_subscription_path(issuable)
+ end
+
+ expose :move_issue_path do |issuable|
+ move_namespace_project_issue_path(
+ namespace_id: issuable.project.namespace.to_param,
+ project_id: issuable.project,
+ id: issuable
+ )
+ end
+
+ expose :projects_autocomplete_path do |issuable|
+ autocomplete_projects_path(project_id: issuable.project.id)
+ end
+
+ private
+
+ def current_user
+ request.current_user
+ end
+end
diff --git a/app/serializers/issuable_sidebar_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index 773d78d324c..d60253564e1 100644
--- a/app/serializers/issuable_sidebar_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-class IssuableSidebarEntity < Grape::Entity
- include TimeTrackableEntity
+class IssuableSidebarExtrasEntity < Grape::Entity
include RequestAwareEntity
+ include TimeTrackableEntity
expose :participants, using: ::API::Entities::UserBasic do |issuable|
issuable.participants(request.current_user)
diff --git a/app/serializers/issuable_sidebar_todo_entity.rb b/app/serializers/issuable_sidebar_todo_entity.rb
new file mode 100644
index 00000000000..b2c98433f05
--- /dev/null
+++ b/app/serializers/issuable_sidebar_todo_entity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class IssuableSidebarTodoEntity < Grape::Entity
+ include Gitlab::Routing
+
+ expose :id
+
+ expose :delete_path do |todo|
+ dashboard_todo_path(todo) if todo
+ end
+end
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index 58ab804a3c8..f7719447b92 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -17,7 +17,7 @@ class IssueBoardEntity < Grape::Entity
end
expose :milestone, expose_nil: false do |issue|
- API::Entities::Project.represent issue.milestone, only: [:id, :title]
+ API::Entities::Milestone.represent issue.milestone, only: [:id, :title]
end
expose :assignees do |issue|
@@ -37,7 +37,7 @@ class IssueBoardEntity < Grape::Entity
end
expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue|
- project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar')
+ project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar_extras')
end
expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue|
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
index d66f0a5acb7..0fa76f098cd 100644
--- a/app/serializers/issue_serializer.rb
+++ b/app/serializers/issue_serializer.rb
@@ -2,13 +2,15 @@
class IssueSerializer < BaseSerializer
# This overrided method takes care of which entity should be used
- # to serialize the `issue` based on `basic` key in `opts` param.
+ # to serialize the `issue` based on `serializer` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(issue, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
- IssueSidebarEntity
+ IssueSidebarBasicEntity
+ when 'sidebar_extras'
+ IssueSidebarExtrasEntity
when 'board'
IssueBoardEntity
else
diff --git a/app/serializers/issue_sidebar_basic_entity.rb b/app/serializers/issue_sidebar_basic_entity.rb
new file mode 100644
index 00000000000..723875809ec
--- /dev/null
+++ b/app/serializers/issue_sidebar_basic_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
+ expose :due_date
+ expose :confidential
+end
diff --git a/app/serializers/issue_sidebar_entity.rb b/app/serializers/issue_sidebar_extras_entity.rb
index 349ad9d1fef..7b6e860140b 100644
--- a/app/serializers/issue_sidebar_entity.rb
+++ b/app/serializers/issue_sidebar_extras_entity.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class IssueSidebarEntity < IssuableSidebarEntity
+class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index f7eb74cf392..084627f9dbe 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MergeRequestBasicEntity < IssuableSidebarEntity
+class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id
expose :merge_status
expose :merge_error
diff --git a/app/serializers/merge_request_basic_serializer.rb b/app/serializers/merge_request_basic_serializer.rb
deleted file mode 100644
index a68b48b00db..00000000000
--- a/app/serializers/merge_request_basic_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class MergeRequestBasicSerializer < BaseSerializer
- entity MergeRequestBasicEntity
-end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 1f8c830e1aa..4cf84336aa4 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -7,9 +7,14 @@ class MergeRequestSerializer < BaseSerializer
def represent(merge_request, opts = {})
entity =
case opts[:serializer]
- when 'basic', 'sidebar'
+ when 'sidebar'
+ MergeRequestSidebarBasicEntity
+ when 'sidebar_extras'
+ IssuableSidebarExtrasEntity
+ when 'basic'
MergeRequestBasicEntity
- else # It's 'widget'
+ else
+ # fallback to widget for old poll requests without `serializer` set
MergeRequestWidgetEntity
end
diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb
new file mode 100644
index 00000000000..0ae7298a7c1
--- /dev/null
+++ b/app/serializers/merge_request_sidebar_basic_entity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
+ expose :assignee, if: lambda { |issuable| issuable.assignee } do
+ expose :assignee, merge: true, using: API::Entities::UserBasic
+
+ expose :can_merge do |issuable|
+ issuable.can_be_merged_by?(issuable.assignee)
+ end
+ end
+end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index f33a1654d5e..9731b52f1ad 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -238,6 +238,8 @@ class MergeRequestWidgetEntity < IssuableEntity
end
end
+ expose :supports_suggestion?, as: :can_receive_suggestion
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index c6d27817411..1d3b59eb1b7 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -36,6 +36,7 @@ class NoteEntity < API::Entities::Note
end
end
+ expose :suggestions, using: SuggestionEntity
expose :resolved?, as: :resolved
expose :resolvable?, as: :resolvable
diff --git a/app/serializers/suggestion_entity.rb b/app/serializers/suggestion_entity.rb
new file mode 100644
index 00000000000..4d0d4da10be
--- /dev/null
+++ b/app/serializers/suggestion_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class SuggestionEntity < API::Entities::Suggestion
+ include RequestAwareEntity
+
+ expose :current_user do
+ expose :can_apply do |suggestion|
+ Ability.allowed?(current_user, :apply_suggestion, suggestion)
+ end
+ end
+
+ private
+
+ def current_user
+ request.current_user
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 19b5552887f..f8d8ef04001 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -31,7 +31,8 @@ module Ci
seeds_block: block,
variables_attributes: params[:variables_attributes],
project: project,
- current_user: current_user)
+ current_user: current_user,
+ push_options: params[:push_options])
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 13321b2682e..6707a1363d0 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -118,7 +118,7 @@ module Ci
# Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL`
groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces)
- hierarchy_groups = Gitlab::GroupHierarchy.new(groups).base_and_descendants
+ hierarchy_groups = Gitlab::ObjectHierarchy.new(groups).base_and_descendants
projects = Project.where(namespace_id: hierarchy_groups)
.with_group_runners_enabled
.with_builds_enabled
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 301059f0326..5525c1b9b7f 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -13,17 +13,17 @@ module Clusters
configure_kubernetes
cluster.save!
- ClusterPlatformConfigureWorker.perform_async(cluster.id)
+ ClusterConfigureWorker.perform_async(cluster.id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
log_service_error(e.class.name, provider.id, e.message)
- provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
+ provider.make_errored!(s_('ClusterIntegration|Failed to request to Google Cloud Platform: %{message}') % { message: e.message })
rescue Kubeclient::HttpError => e
log_service_error(e.class.name, provider.id, e.message)
- provider.make_errored!("Failed to run Kubeclient: #{e.message}")
+ provider.make_errored!(s_('ClusterIntegration|Failed to run Kubeclient: %{message}') % { message: e.message })
rescue ActiveRecord::RecordInvalid => e
log_service_error(e.class.name, provider.id, e.message)
- provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}")
+ provider.make_errored!(s_('ClusterIntegration|Failed to configure Google Kubernetes Engine Cluster: %{message}') % { message: e.message })
end
private
diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb
index 7961ba4d3c4..bb8cfb63f98 100644
--- a/app/services/commits/tag_service.rb
+++ b/app/services/commits/tag_service.rb
@@ -9,11 +9,10 @@ module Commits
tag_name = params[:tag_name]
message = params[:tag_message]
- release_description = nil
result = Tags::CreateService
.new(commit.project, current_user)
- .execute(tag_name, commit.sha, message, release_description)
+ .execute(tag_name, commit.sha, message)
if result[:status] == :success
tag = result[:tag]
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
deleted file mode 100644
index 8d1fdbe11c3..00000000000
--- a/app/services/create_release_service.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-class CreateReleaseService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(tag_name, release_description)
- repository = project.repository
- existing_tag = repository.find_tag(tag_name)
-
- # Only create a release if the tag exists
- if existing_tag
- release = project.releases.find_by(tag: tag_name)
-
- if release
- error('Release already exists', 409)
- else
- release = project.releases.new({ tag: tag_name, description: release_description })
- release.save
-
- success(release)
- end
- else
- error('Tag does not exist', 404)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def success(release)
- super().merge(release: release)
- end
-end
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
index a25e73666f8..0c935285657 100644
--- a/app/services/deploy_keys/create_service.rb
+++ b/app/services/deploy_keys/create_service.rb
@@ -2,7 +2,7 @@
module DeployKeys
class CreateService < Keys::BaseService
- def execute
+ def execute(project: nil)
DeployKey.create(params.merge(user: user))
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index f1883877d56..9ecee7c6156 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -174,7 +174,8 @@ class GitPushService < BaseService
params[:newrev],
params[:ref],
@push_commits,
- commits_count: commits_count)
+ commits_count: commits_count,
+ push_options: params[:push_options] || [])
end
def push_to_existing_branch?
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index dbadafc0f52..03fcf614c64 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -45,7 +45,8 @@ class GitTagPushService < BaseService
params[:newrev],
params[:ref],
commits,
- message)
+ message,
+ push_options: params[:push_options] || [])
end
def build_system_push_data
diff --git a/app/services/groups/nested_create_service.rb b/app/services/groups/nested_create_service.rb
index 50d34d8cb91..f01f5656296 100644
--- a/app/services/groups/nested_create_service.rb
+++ b/app/services/groups/nested_create_service.rb
@@ -18,7 +18,7 @@ module Groups
return namespace
end
- if group_path.include?('/') && !Group.supports_nested_groups?
+ if group_path.include?('/') && !Group.supports_nested_objects?
raise 'Nested groups are not supported on MySQL'
end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 5efa746dfb9..f64e327416a 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -40,7 +40,7 @@ module Groups
def ensure_allowed_transfer
raise_transfer_error(:group_is_already_root) if group_is_already_root?
- raise_transfer_error(:database_not_supported) unless Group.supports_nested_groups?
+ raise_transfer_error(:database_not_supported) unless Group.supports_nested_objects?
raise_transfer_error(:same_parent_as_current) if same_parent?
raise_transfer_error(:invalid_policies) unless valid_policies?
raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 0bf0e967dcc..de78a3f7b27 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -31,12 +31,12 @@ module Groups
def after_update
if group.previous_changes.include?(:visibility_level) && group.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::GroupPrivateWorker.perform_in(1.hour, group.id)
+ TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id)
end
end
def reject_parent_id!
- params.except!(:parent_id)
+ params.delete(:parent_id)
end
def valid_share_with_group_lock_change?
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 765de9c66b0..885e14bba8f 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -4,20 +4,23 @@ module Issuable
class CommonSystemNotesService < ::BaseService
attr_reader :issuable
- def execute(issuable, old_labels)
+ def execute(issuable, old_labels: [], is_update: true)
@issuable = issuable
- if issuable.previous_changes.include?('title')
- create_title_change_note(issuable.previous_changes['title'].first)
- end
+ if is_update
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable.previous_changes['title'].first)
+ end
- handle_description_change_note
+ handle_description_change_note
+
+ handle_time_tracking_note if issuable.is_a?(TimeTrackable)
+ create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
+ end
- handle_time_tracking_note if issuable.is_a?(TimeTrackable)
- create_labels_note(old_labels) if issuable.labels != old_labels
- create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked')
- create_milestone_note if issuable.previous_changes.include?('milestone_id')
create_due_date_note if issuable.previous_changes.include?('due_date')
+ create_milestone_note if issuable.previous_changes.include?('milestone_id')
+ create_labels_note(old_labels) if issuable.labels != old_labels
end
private
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e32e262ac31..c7e7bb55e4b 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -152,6 +152,10 @@ class IssuableBaseService < BaseService
before_create(issuable)
if issuable.save
+ ActiveRecord::Base.no_touching do
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, is_update: false)
+ end
+
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
@@ -207,7 +211,7 @@ class IssuableBaseService < BaseService
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_associations[:labels])
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels])
end
handle_changes(issuable, old_associations: old_associations)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a1d0cc0e568..e992d682c79 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -44,7 +44,7 @@ module Issues
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::ConfidentialIssueWorker.perform_in(1.hour, issue.id) if issue.confidential?
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
end
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index f30ad706c63..3c0e6196d4f 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -57,7 +57,7 @@ module Labels
def update_issuables(new_label, label_ids)
LabelLink
.where(label: label_ids)
- .update_all(label_id: new_label)
+ .update_all(label_id: new_label.id)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -65,7 +65,7 @@ module Labels
def update_resource_label_events(new_label, label_ids)
ResourceLabelEvent
.where(label: label_ids)
- .update_all(label_id: new_label)
+ .update_all(label_id: new_label.id)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -73,7 +73,7 @@ module Labels
def update_issue_board_lists(new_label, label_ids)
List
.where(label: label_ids)
- .update_all(label_id: new_label)
+ .update_all(label_id: new_label.id)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -81,7 +81,7 @@ module Labels
def update_priorities(new_label, label_ids)
LabelPriority
.where(label: label_ids)
- .update_all(label_id: new_label)
+ .update_all(label_id: new_label.id)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index d734571f835..e78affff797 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -47,5 +47,11 @@ module Members
raise "Unknown action '#{action}' on #{member}!"
end
end
+
+ def enqueue_delete_todos(member)
+ type = member.is_a?(GroupMember) ? 'Group' : 'Project'
+ # don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type)
+ end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index c186a5971dc..ae0c644e6c0 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -15,7 +15,7 @@ module Members
notification_service.decline_access_request(member)
end
- enqeue_delete_todos(member)
+ enqueue_delete_todos(member)
after_execute(member: member)
@@ -24,12 +24,6 @@ module Members
private
- def enqeue_delete_todos(member)
- type = member.is_a?(GroupMember) ? 'Group' : 'Project'
- # don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::EntityLeaveWorker.perform_in(1.hour, member.user_id, member.source_id, type)
- end
-
def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member)
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 1f5618dae53..ff8d5c1d8c9 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -10,9 +10,18 @@ module Members
if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
+
+ # Deletes only confidential issues todos for guests
+ enqueue_delete_todos(member) if downgrading_to_guest?
end
member
end
+
+ private
+
+ def downgrading_to_guest?
+ params[:access_level] == Gitlab::Access::GUEST
+ end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 36767621d74..48419da98ad 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -18,7 +18,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
- merge_request.can_be_created = branches_valid?
+ merge_request.can_be_created = projects_and_branches_valid?
# compare branches only if branches are valid, otherwise
# compare_branches may raise an error
@@ -49,15 +49,19 @@ module MergeRequests
to: :merge_request
def find_source_project
- return source_project if source_project.present? && can?(current_user, :read_project, source_project)
+ return source_project if source_project.present? && can?(current_user, :create_merge_request_from, source_project)
project
end
def find_target_project
- return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+ return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
- project.default_merge_request_target
+ target_project = project.default_merge_request_target
+
+ return target_project if target_project.present? && can?(current_user, :create_merge_request_in, target_project)
+
+ project
end
def find_target_branch
@@ -72,10 +76,11 @@ module MergeRequests
params[:target_branch].present?
end
- def branches_valid?
+ def projects_and_branches_valid?
+ return false if source_project.nil? || target_project.nil?
return false unless source_branch_specified? || target_branch_specified?
- validate_branches
+ validate_projects_and_branches
errors.blank?
end
@@ -94,7 +99,12 @@ module MergeRequests
end
end
- def validate_branches
+ def validate_projects_and_branches
+ merge_request.validate_target_project
+ merge_request.validate_fork
+
+ return if errors.any?
+
add_error('You must select source and target branch') unless branches_present?
add_error('You must select different branches') if same_source_and_target?
add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index aacaf10d09c..86a04587f79 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -5,14 +5,15 @@ module MergeRequests
def execute(merge_request)
# We don't allow change of source/target projects and source branch
# after merge request was created
- params.except!(:source_project_id)
- params.except!(:target_project_id)
- params.except!(:source_branch)
+ params.delete(:source_project_id)
+ params.delete(:target_project_id)
+ params.delete(:source_branch)
merge_from_quick_action(merge_request) if params[:merge]
if merge_request.closed_without_fork?
- params.except!(:target_branch, :force_remove_source_branch)
+ params.delete(:target_branch)
+ params.delete(:force_remove_source_branch)
end
if params[:force_remove_source_branch].present?
@@ -45,11 +46,13 @@ module MergeRequests
end
if merge_request.previous_changes.include?('assignee_id')
+ reassigned_merge_request_args = [merge_request, current_user]
+
old_assignee_id = merge_request.previous_changes['assignee_id'].first
- old_assignee = User.find(old_assignee_id) if old_assignee_id
+ reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
create_assignee_note(merge_request)
- notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignee)
+ notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
todo_service.reassigned_merge_request(merge_request, current_user)
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index e03789e3ca9..c4546f30235 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -36,6 +36,7 @@ module Notes
if !only_commands && note.save
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
+ Suggestions::CreateService.new(note).execute
end
if command_params.present?
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 35db409eb27..d2052bed646 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -14,6 +14,17 @@ module Notes
TodoService.new.update_note(note, current_user, old_mentioned_users)
end
+ if note.supports_suggestion?
+ Suggestion.transaction do
+ note.suggestions.delete_all
+ Suggestions::CreateService.new(note).execute
+ end
+
+ # We need to refresh the previous suggestions call cache
+ # in order to get the new records.
+ note.reload
+ end
+
note
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ff035fea216..e1cf327209b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -188,7 +188,7 @@ class NotificationService
# * merge_request assignee if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user, previous_assignee)
+ def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index de8757006f1..a449a5dc3e9 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -4,10 +4,12 @@ class PreviewMarkdownService < BaseService
def execute
text, commands = explain_quick_actions(params[:text])
users = find_user_references(text)
+ suggestions = find_suggestions(text)
success(
text: text,
users: users,
+ suggestions: suggestions,
commands: commands.join(' '),
markdown_engine: markdown_engine
)
@@ -28,6 +30,12 @@ class PreviewMarkdownService < BaseService
extractor.users.map(&:username)
end
+ def find_suggestions(text)
+ return [] unless params[:preview_suggestions]
+
+ Banzai::SuggestionsParser.parse(text)
+ end
+
def find_commands_target
QuickActions::TargetService
.new(project, current_user)
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index 4131da44f5a..aa9b253eb20 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -81,6 +81,7 @@ module Projects
def update_repository_configuration
project.reload_repository!
project.write_repository_config
+ project.track_project_repository
end
def rename_transferred_documents
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 210571b6b4e..336d029d330 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -137,6 +137,8 @@ module Projects
raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
end
+ project.leave_pool_repository
+
Project.transaction do
log_destroy_event
trash_repositories!
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 1c4a8d05be6..b5128443435 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -4,33 +4,51 @@
module Projects
module LfsPointers
class LfsDownloadService < BaseService
+ VALID_PROTOCOLS = %w[http https].freeze
+
# rubocop: disable CodeReuse/ActiveRecord
def execute(oid, url)
return unless project&.lfs_enabled? && oid.present? && url.present?
return if LfsObject.exists?(oid: oid)
- sanitized_uri = Gitlab::UrlSanitizer.new(url)
+ sanitized_uri = sanitize_url!(url)
with_tmp_file(oid) do |file|
- size = download_and_save_file(file, sanitized_uri)
- lfs_object = LfsObject.new(oid: oid, size: size, file: file)
+ download_and_save_file(file, sanitized_uri)
+ lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
project.all_lfs_objects << lfs_object
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} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
+ 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)
+ end
+ end
+
def download_and_save_file(file, sanitized_uri)
- IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
+ response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
+ file.write(fragment)
+ end
+
+ raise StandardError, "Received error code #{response.code}" unless response.success?
end
def headers(sanitized_uri)
- {}.tap do |headers|
+ query_options.tap do |headers|
credentials = sanitized_uri.credentials
if credentials[:user].present? || credentials[:password].present?
@@ -40,10 +58,14 @@ module Projects
end
end
+ def query_options
+ { stream_body: true }
+ end
+
def with_tmp_file(oid)
create_tmp_storage_dir
- File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file }
+ File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
end
def create_tmp_storage_dir
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 9db3fd9cf17..5da1e39a1fb 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -81,7 +81,7 @@ module Projects
project.old_path_with_namespace = @old_path
- write_repository_config(@new_path)
+ update_repository_configuration(@new_path)
execute_system_hooks
end
@@ -106,8 +106,9 @@ module Projects
project.save!
end
- def write_repository_config(full_path)
+ def update_repository_configuration(full_path)
project.write_repository_config(gl_full_path: full_path)
+ project.track_project_repository
end
def refresh_permissions
@@ -123,7 +124,7 @@ module Projects
rollback_folder_move
project.reload
update_namespace_and_visibility(@old_namespace)
- write_repository_config(@old_path)
+ update_repository_configuration(@old_path)
end
def rollback_folder_move
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 93e48fc0199..dd1b9680ece 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -61,9 +61,9 @@ module Projects
if project.previous_changes.include?(:visibility_level) && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
- TodosDestroyer::ProjectPrivateWorker.perform_in(1.hour, project.id)
+ TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
elsif (project_changed_feature_keys & todos_features_changes).present?
- TodosDestroyer::PrivateFeaturesWorker.perform_in(1.hour, project.id)
+ TodosDestroyer::PrivateFeaturesWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
end
if project.previous_changes.include?('path')
diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb
new file mode 100644
index 00000000000..a04bb8f9e14
--- /dev/null
+++ b/app/services/releases/concerns.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Releases
+ module Concerns
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ included do
+ def tag_name
+ params[:tag]
+ end
+
+ def ref
+ params[:ref]
+ end
+
+ def name
+ params[:name]
+ end
+
+ def description
+ params[:description]
+ end
+
+ def release
+ strong_memoize(:release) do
+ project.releases.find_by_tag(tag_name)
+ end
+ end
+
+ def existing_tag
+ strong_memoize(:existing_tag) do
+ repository.find_tag(tag_name)
+ end
+ end
+
+ def tag_exist?
+ existing_tag.present?
+ end
+
+ def repository
+ strong_memoize(:repository) do
+ project.repository
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
new file mode 100644
index 00000000000..73fcebf79af
--- /dev/null
+++ b/app/services/releases/create_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Releases
+ class CreateService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Access Denied', 403) unless allowed?
+ return error('Release already exists', 409) if release
+
+ tag = ensure_tag
+
+ return tag unless tag.is_a?(Gitlab::Git::Tag)
+
+ create_release(tag)
+ end
+
+ private
+
+ def ensure_tag
+ existing_tag || create_tag
+ end
+
+ def create_tag
+ return error('Ref is not specified', 422) unless ref
+
+ result = Tags::CreateService
+ .new(project, current_user)
+ .execute(tag_name, ref, nil)
+
+ return result unless result[:status] == :success
+
+ result[:tag]
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :create_release, project)
+ end
+
+ def create_release(tag)
+ release = project.releases.create!(
+ name: name,
+ description: description,
+ author: current_user,
+ tag: tag.name,
+ sha: tag.dereferenced_target.sha
+ )
+
+ success(tag: tag, release: release)
+ rescue ActiveRecord::RecordInvalid => e
+ error(e.message, 400)
+ end
+ end
+end
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
new file mode 100644
index 00000000000..8c2bc3b4e6e
--- /dev/null
+++ b/app/services/releases/destroy_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Releases
+ class DestroyService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Tag does not exist', 404) unless existing_tag
+ return error('Release does not exist', 404) unless release
+ return error('Access Denied', 403) unless allowed?
+
+ if release.destroy
+ success(tag: existing_tag, release: release)
+ else
+ error(release.errors.messages || '400 Bad request', 400)
+ end
+ end
+
+ private
+
+ def allowed?
+ Ability.allowed?(current_user, :destroy_release, release)
+ end
+ end
+end
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
new file mode 100644
index 00000000000..fabfa398c59
--- /dev/null
+++ b/app/services/releases/update_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Releases
+ class UpdateService < BaseService
+ include Releases::Concerns
+
+ def execute
+ return error('Tag does not exist', 404) unless existing_tag
+ return error('Release does not exist', 404) unless release
+ return error('Access Denied', 403) unless allowed?
+ return error('params is empty', 400) if empty_params?
+
+ if release.update(params)
+ success(tag: existing_tag, release: release)
+ else
+ error(release.errors.messages || '400 Bad request', 400)
+ end
+ end
+
+ private
+
+ def allowed?
+ Ability.allowed?(current_user, :update_release, release)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def empty_params?
+ params.except(:tag).empty?
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
new file mode 100644
index 00000000000..d931d528c86
--- /dev/null
+++ b/app/services/suggestions/apply_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Suggestions
+ class ApplyService < ::BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(suggestion)
+ unless suggestion.appliable?
+ return error('Suggestion is not appliable')
+ end
+
+ params = file_update_params(suggestion)
+ result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute
+
+ if result[:status] == :success
+ suggestion.update(commit_id: result[:result], applied: true)
+ end
+
+ result
+ end
+
+ private
+
+ def file_update_params(suggestion)
+ diff_file = suggestion.diff_file
+
+ file_path = diff_file.file_path
+ branch_name = suggestion.noteable.source_branch
+ file_content = new_file_content(suggestion)
+ commit_message = "Apply suggestion to #{file_path}"
+
+ {
+ file_path: file_path,
+ branch_name: branch_name,
+ start_branch: branch_name,
+ commit_message: commit_message,
+ file_content: file_content
+ }
+ end
+
+ def new_file_content(suggestion)
+ range = suggestion.from_line_index..suggestion.to_line_index
+ blob = suggestion.diff_file.new_blob
+
+ blob.load_all_data!
+ content = blob.data.lines
+ content[range] = suggestion.to_content
+
+ content.join
+ end
+ end
+end
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
new file mode 100644
index 00000000000..77e958cbe0c
--- /dev/null
+++ b/app/services/suggestions/create_service.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Suggestions
+ class CreateService
+ def initialize(note)
+ @note = note
+ end
+
+ def execute
+ return unless @note.supports_suggestion?
+
+ suggestions = Banzai::SuggestionsParser.parse(@note.note)
+
+ # For single line suggestion we're only looking forward to
+ # change the line receiving the comment. Though, in
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/53310
+ # we'll introduce a ```suggestion:L<x>-<y>, so this will
+ # slightly change.
+ comment_line = @note.position.new_line
+
+ rows =
+ suggestions.map.with_index do |suggestion, index|
+ from_content = changing_lines(comment_line, comment_line)
+
+ # The parsed suggestion doesn't have information about the correct
+ # ending characters (we may have a line break, or not), so we take
+ # this information from the last line being changed (last
+ # characters).
+ endline_chars = line_break_chars(from_content.lines.last)
+ to_content = "#{suggestion}#{endline_chars}"
+
+ {
+ note_id: @note.id,
+ from_content: from_content,
+ to_content: to_content,
+ relative_order: index
+ }
+ end
+
+ rows.in_groups_of(100, false) do |rows|
+ Gitlab::Database.bulk_insert('suggestions', rows)
+ end
+ end
+
+ private
+
+ def changing_lines(from_line, to_line)
+ @note.diff_file.new_blob_lines_between(from_line, to_line).join
+ end
+
+ def line_break_chars(line)
+ match = /\r\n|\r|\n/.match(line)
+ match[0] if match
+ end
+ end
+end
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index 35390f5082c..4de6b2d2774 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -2,7 +2,7 @@
module Tags
class CreateService < BaseService
- def execute(tag_name, target, message, release_description = nil)
+ def execute(tag_name, target, message)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag
@@ -20,10 +20,7 @@ module Tags
end
if new_tag
- if release_description
- CreateReleaseService.new(@project, @current_user)
- .execute(tag_name, release_description)
- end
+ repository.expire_tags_cache
success.merge(tag: new_tag)
else
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 6bfef09ac54..cab507946b4 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -2,7 +2,6 @@
module Tags
class DestroyService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
@@ -12,8 +11,12 @@ module Tags
end
if repository.rm_tag(current_user, tag_name)
- release = project.releases.find_by(tag: tag_name)
- release&.destroy
+ ##
+ # When a tag in a repository is destroyed,
+ # release assets will be destroyed too.
+ Releases::DestroyService
+ .new(project, current_user, tag: tag_name)
+ .execute
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
@@ -27,7 +30,6 @@ module Tags
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
end
- # rubocop: enable CodeReuse/ActiveRecord
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
deleted file mode 100644
index e2228ca026c..00000000000
--- a/app/services/update_release_service.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateReleaseService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
- def execute(tag_name, release_description)
- repository = project.repository
- existing_tag = repository.find_tag(tag_name)
-
- if existing_tag
- release = project.releases.find_by(tag: tag_name)
-
- if release
- release.update(description: release_description)
-
- success(release)
- else
- error('Release does not exist', 404)
- end
- else
- error('Tag does not exist', 404)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def success(release)
- super().merge(release: release)
- end
-end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 23b63aaabdf..fe5a82e23fa 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -102,7 +102,7 @@ module Users
end
def fresh_authorizations
- klass = if Group.supports_nested_groups?
+ klass = if Group.supports_nested_objects?
Gitlab::ProjectAuthorizations::WithNestedGroups
else
Gitlab::ProjectAuthorizations::WithoutNestedGroups
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index a897e4bd56a..af4fe1aebb9 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -16,7 +16,7 @@ module Users
user_exists = @user.persisted?
- assign_attributes(&block)
+ assign_attributes
if @user.save(validate: validate) && update_status
notify_success(user_exists)
@@ -48,9 +48,11 @@ module Users
success
end
- def assign_attributes(&block)
- if @user.user_synced_attributes_metadata
- params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
+ def assign_attributes
+ if (metadata = @user.user_synced_attributes_metadata)
+ read_only = metadata.read_only_attributes
+
+ params.reject! { |key, _| read_only.include?(key.to_sym) }
end
@user.assign_attributes(params) if params.any?
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index cb67079853e..544f09048f5 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -8,7 +8,7 @@
= f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.header_logo?
- = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ = image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
@@ -25,7 +25,7 @@
= f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.favicon?
- = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
+ = image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
@@ -54,7 +54,7 @@
= f.label :logo, class: 'col-sm-2 col-form-label pt-0'
.col-sm-10
- if @appearance.logo?
- = image_tag @appearance.logo_url, class: 'appearance-logo-preview'
+ = image_tag @appearance.logo_path, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
= link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 0d42094fc89..fdaad1cf181 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -49,5 +49,12 @@
Once that time passes, the jobs will be archived and no longer able to be
retried. Make it empty to never expire jobs. It has to be no less than 1 day,
for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.
+ .form-group
+ .form-check
+ = f.check_box :protected_ci_variables, class: 'form-check-input'
+ = f.label :protected_ci_variables, class: 'form-check-label' do
+ = s_('AdminSettings|Environment variables are protected by default')
+ .form-text.text-muted
+ = s_('AdminSettings|When creating a new environment variable it will be protected by default.')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml
index 19c2a50ebd9..4f4f0a543e0 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/admin/runners/_sort_dropdown.html.haml
@@ -6,6 +6,6 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
- = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date, label: true), sorted_by)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sorted_by)
+ = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date), sorted_by)
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index d355e7799df..90c59bec975 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1 +1,3 @@
-= _('Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want.')
+= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want.')
+= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
+= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
new file mode 100644
index 00000000000..cb7779e2175
--- /dev/null
+++ b/app/views/ci/variables/_header.html.haml
@@ -0,0 +1,11 @@
+- expanded = local_assigns.fetch(:expanded)
+
+%h4
+ = _('Environment variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
+
+%button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+
+%p.append-bottom-0
+ = render "ci/variables/content"
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index f34305e94fa..dc9ccb6cc39 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -1,5 +1,10 @@
- save_endpoint = local_assigns.fetch(:save_endpoint, nil)
+- if ci_variable_protected_by_default?
+ %p.settings-message.text-center
+ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') }
+ = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+
.row
.col-lg-12.js-ci-variable-list-section{ data: { save_endpoint: save_endpoint } }
.hide.alert.alert-danger.js-ci-variable-error-box
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 6ee55836dd2..16a7527c8ce 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -5,7 +5,8 @@
- id = variable&.id
- key = variable&.key
- value = variable&.value
-- is_protected = variable && !only_key_value ? variable.protected : false
+- is_protected_default = ci_variable_protected_by_default?
+- is_protected = ci_variable_protected?(variable, only_key_value)
- id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
@@ -39,7 +40,8 @@
%input{ type: "hidden",
class: 'js-ci-variable-input-protected js-project-feature-toggle-input',
name: protected_input_name,
- value: is_protected }
+ value: is_protected,
+ data: { default: is_protected_default.to_s } }
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
diff --git a/app/views/clusters/clusters/_buttons.html.haml b/app/views/clusters/clusters/_buttons.html.haml
index db2e247e341..c81d1d5b05a 100644
--- a/app/views/clusters/clusters/_buttons.html.haml
+++ b/app/views/clusters/clusters/_buttons.html.haml
@@ -1,4 +1,6 @@
--# This partial is overridden in EE
.nav-controls
- %span.btn.btn-add-cluster.disabled.js-add-cluster
- = s_("ClusterIntegration|Add Kubernetes cluster")
+ - if clusterable.can_add_cluster?
+ = link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success js-add-cluster'
+ - else
+ %span.btn.btn-add-cluster.disabled.js-add-cluster
+ = s_("ClusterIntegration|Add Kubernetes cluster")
diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
index adeca013749..b89789e9915 100644
--- a/app/views/clusters/clusters/_cluster.html.haml
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -3,7 +3,7 @@
.table-section.section-60
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
- = link_to cluster.name, cluster.show_path
+ = cluster.item_link(clusterable)
- unless cluster.enabled?
%span.badge.badge-danger Connection disabled
.table-section.section-25
@@ -13,4 +13,4 @@
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%span.badge.badge-light
- = cluster.project_type? ? s_("ClusterIntegration|Project cluster") : s_("ClusterIntegration|Group cluster")
+ = cluster.cluster_type_description
diff --git a/app/views/clusters/clusters/_empty_state.html.haml b/app/views/clusters/clusters/_empty_state.html.haml
index c926ec258f0..cfdbfe2dea1 100644
--- a/app/views/clusters/clusters/_empty_state.html.haml
+++ b/app/views/clusters/clusters/_empty_state.html.haml
@@ -9,6 +9,6 @@
= clusterable.empty_state_help_text
= clusterable.learn_more_link
- - if clusterable.can_create_cluster?
+ - if clusterable.can_add_cluster?
.text-center
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), clusterable.new_path, class: 'btn btn-success'
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 73b11d509d3..85d1002243b 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,6 +1,6 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
-.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
- %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
+ %button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index ad6d1d856d6..58d0a304363 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -11,6 +11,13 @@
.nav-text
= 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.")
+ %strong
+ = link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
+
.clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-60{ role: "rowheader" }
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 9c246e19faa..4359a2c3c2b 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,7 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
+ = link_to dashboard_projects_path(rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss
.content_list
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 31d4b3da4f1..4dbda5c754b 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+
+= render_if_exists "shared/gold_trial_callout"
+
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 50f39f93283..2f7add600e4 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,6 +1,8 @@
- @hide_top_links = true
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
+
+= render_if_exists "shared/gold_trial_callout"
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index fdd5c19d562..afd46412fab 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Issues')
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 77cfa1271df..3e5f13b92e3 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,6 +2,8 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Merge Requests')
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index b876d6fd1f3..89212eb6bf9 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,5 +1,5 @@
= render 'shared/milestones/milestone',
- milestone_path: group_or_dashboard_milestone_path(milestone),
+ milestone_path: group_or_project_milestone_path(milestone),
issues_path: issues_dashboard_path(milestone_title: milestone.title),
merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title),
milestone: milestone,
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index deed774a4a5..446b4715b2d 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+= render_if_exists "shared/gold_trial_callout"
+
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 8933d9e31ff..ad08409c8fe 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -4,6 +4,8 @@
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
%div{ class: container_class }
= render "projects/last_push"
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d2593179f17..47729321961 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,6 +2,8 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
+= render_if_exists "shared/gold_trial_callout"
+
.page-title-holder
%h1.page-title= _('Todos')
diff --git a/app/views/events/_events.html.haml b/app/views/events/_events.html.haml
index 68c19df092d..6ae4c334f7f 100644
--- a/app/views/events/_events.html.haml
+++ b/app/views/events/_events.html.haml
@@ -1 +1,4 @@
-= render partial: 'events/event', collection: @events
+- if @events.present?
+ = render partial: 'events/event', collection: @events
+- else
+ .nothing-here-block= _("No activities found")
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index a3eafc61d0a..869be4e8581 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -2,6 +2,8 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/groups_head'
- else
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 452f390695c..d18dec7bd8e 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
+= render_if_exists "shared/gold_trial_callout"
+
- if current_user
= render 'dashboard/projects_head'
- else
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 82a497289f3..13df1e57125 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,7 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
+ = link_to group_path(@group, rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
.content_list
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 6219da2c715..88e401081f4 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -12,6 +12,6 @@
= markdown_field(@group, :description)
- if current_user
- .group-buttons
+ .group-buttons.d-none.d-sm-block
= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index a5e6abdba52..d9332e36ef5 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -5,13 +5,7 @@
%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
- %h4
- = _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
- %button.btn.btn-default.js-settings-toggle{ type: "button" }
- = expanded ? _('Collapse') : _('Expand')
- %p.append-bottom-0
- = render "ci/variables/content"
+ = render 'ci/variables/header', expanded: expanded
.settings-content
= render 'ci/variables/index', save_endpoint: group_variables_path
diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby
index 73ab8489e0c..94c3099ace2 100644
--- a/app/views/issues/_issues_calendar.ics.ruby
+++ b/app/views/issues/_issues_calendar.ics.ruby
@@ -3,7 +3,7 @@ cal.prodid = '-//GitLab//NONSGML GitLab//EN'
cal.x_wr_calname = 'GitLab Issues'
# rubocop: disable CodeReuse/ActiveRecord
-@issues.includes(project: :namespace).each do |issue|
+@issues.preload(project: :namespace).each do |issue|
cal.event do |event|
event.dtstart = Icalendar::Values::Date.new(issue.due_date)
event.summary = "#{issue.title} (in #{issue.project.full_path})"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index a86972d8cf3..a6023a1cbb9 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,7 +2,7 @@
- group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
-.search.search-form
+.search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input" } }
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
.search-input-wrap
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 4f8db74382f..6003d973c88 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
- %body.ui-indigo.login-page.application.navless{ data: { page: body_data_page } }
+ %body.ui-indigo.login-page.application.navless.qa-login-page{ data: { page: body_data_page } }
.page-wrap
= render "layouts/header/empty"
.login-page-broadcast
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e8d0d809181..a9b85889846 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -60,7 +60,7 @@
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
- if header_link?(:user_dropdown)
- %li.nav-item.header-user.dropdown
+ %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown" } }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 953c0e7f46c..04409408ce0 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -2,5 +2,8 @@
- if current_user_menu?(:help)
%li
= link_to _("Help"), help_path
+ %li.divider
+ %li
+ = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 5cb8aebadb3..e42251f9ec8 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,4 +1,4 @@
-%li.header-new.dropdown
+%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 7057a5a142f..ddd30efe062 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -2,7 +2,7 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } }
= _('Projects')
= sprite_icon('angle-down', css_class: 'caret-down')
@@ -10,7 +10,7 @@
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown" }) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } }
= _('Groups')
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 477030a20c1..bf475c07711 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -103,19 +103,6 @@
= _('Merge Requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
- - if group_sidebar_link?(:group_members)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group) do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name.qa-group-members-item
- = _('Members')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to group_group_members_path(@group) do
- %strong.fly-out-top-item-name
- = _('Members')
-
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
@@ -129,6 +116,19 @@
%strong.fly-out-top-item-name
= _('Kubernetes')
+ - if group_sidebar_link?(:group_members)
+ = nav_link(path: 'group_members#index') do
+ = link_to group_group_members_path(@group) do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name.qa-group-members-item
+ = _('Members')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_group_members_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Members')
+
- if group_sidebar_link?(:settings)
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index bdd0108db0d..d8017742c90 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -29,6 +29,11 @@
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span= _('Activity')
+ - if project_nav_tab?(:releases) && Feature.enabled?(:releases_page, @project)
+ = nav_link(controller: :releases) do
+ = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
+ %span= _('Releases')
+
= render_if_exists 'projects/sidebar/security_dashboard'
- if can?(current_user, :read_cycle_analytics, @project)
@@ -62,7 +67,7 @@
= link_to project_branches_path(@project) do
= _('Branches')
- = nav_link(controller: [:tags, :releases]) do
+ = nav_link(controller: [:tags]) do
= link_to project_tags_path(@project) do
= _('Tags')
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index 1fbae2f64ed..83c7f548975 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -4,17 +4,13 @@
- note_style = local_assigns.fetch(:note_style, "")
- discussion = note.discussion if note.part_of_discussion?
-- diff_discussion = discussion&.diff_discussion?
-- on_image = discussion.on_image? if diff_discussion
- if discussion
- - phrase_end_char = on_image ? "." : ":"
-
%p{ style: "color: #777777;" }
- = succeed phrase_end_char do
+ = succeed ':' do
= link_to note.author_name, user_url(note.author)
- - if diff_discussion
+ - if discussion&.diff_discussion?
- if discussion.new_discussion?
started a new discussion
- else
@@ -31,7 +27,7 @@
%p.details
#{link_to note.author_name, user_url(note.author)} commented:
-- if diff_discussion && !on_image
+- if discussion&.diff_discussion? && discussion.on_text?
= content_for :head do
= stylesheet_link_tag 'mailers/highlighted_diff_email'
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index 4bf252b6ce1..50209c46ed1 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -20,7 +20,7 @@
<% end -%>
-<% if discussion&.diff_discussion? -%>
+<% if discussion&.diff_discussion? && discussion.on_text? -%>
<% discussion.truncated_diff_lines(highlight: false, diff_limit: diff_limit).each do |line| -%>
<%= "> #{line.text}\n" -%>
<% end -%>
diff --git a/app/views/notify/changed_milestone_issue_email.html.haml b/app/views/notify/changed_milestone_email.html.haml
index 7d5425fc72d..01d27cac36b 100644
--- a/app/views/notify/changed_milestone_issue_email.html.haml
+++ b/app/views/notify/changed_milestone_email.html.haml
@@ -1,3 +1,5 @@
%p
Milestone changed to
%strong= link_to(@milestone.name, @milestone_url)
+ - if date_range = milestone_date_range(@milestone)
+ = "(#{date_range})"
diff --git a/app/views/notify/changed_milestone_email.text.erb b/app/views/notify/changed_milestone_email.text.erb
new file mode 100644
index 00000000000..a466da4eb19
--- /dev/null
+++ b/app/views/notify/changed_milestone_email.text.erb
@@ -0,0 +1 @@
+Milestone changed to <%= @milestone.name %><% if date_range = milestone_date_range(@milestone) %> (<%= date_range %>)<% end %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/changed_milestone_issue_email.text.erb b/app/views/notify/changed_milestone_issue_email.text.erb
deleted file mode 100644
index c5fc0b61518..00000000000
--- a/app/views/notify/changed_milestone_issue_email.text.erb
+++ /dev/null
@@ -1 +0,0 @@
-Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/notify/changed_milestone_merge_request_email.html.haml b/app/views/notify/changed_milestone_merge_request_email.html.haml
deleted file mode 100644
index 7d5425fc72d..00000000000
--- a/app/views/notify/changed_milestone_merge_request_email.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%p
- Milestone changed to
- %strong= link_to(@milestone.name, @milestone_url)
diff --git a/app/views/notify/changed_milestone_merge_request_email.text.erb b/app/views/notify/changed_milestone_merge_request_email.text.erb
deleted file mode 100644
index c5fc0b61518..00000000000
--- a/app/views/notify/changed_milestone_merge_request_email.text.erb
+++ /dev/null
@@ -1 +0,0 @@
-Milestone changed to <%= @milestone.name %> ( <%= @milestone_url %> )
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 2603c558c0f..2629b374e7c 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -71,43 +71,43 @@
%h4.prepend-top-0
= s_("Profiles|Main settings")
%p
- = s_("Profiles|This information will appear on your profile.")
+ = s_("Profiles|This information will appear on your profile")
- if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8
.row
- if @user.read_only_attribute?(:name)
= f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
- help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) }
+ help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) }
- else
- = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
+ = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- if @user.read_only_attribute?(:email)
- = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) }
+ = f.text_field :email, required: true, class: 'input-lg', readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) }
- else
- = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
+ = f.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?),
help: user_email_help_text(@user)
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
- { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
- control_class: 'select2'
+ { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
+ control_class: 'select2 input-lg'
- commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank')
= f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)),
{ help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } },
- control_class: 'select2'
+ control_class: 'select2 input-lg'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
- { help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
- control_class: 'select2'
- = f.text_field :skype
- = f.text_field :linkedin
- = f.text_field :twitter
- = f.text_field :website_url, label: s_("Profiles|Website")
+ { help: s_("Profiles|This feature is experimental and translations are not complete yet") },
+ control_class: 'select2 input-lg'
+ = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
+ = f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
+ = f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username")
+ = f.text_field :website_url, class: 'input-lg', placeholder: s_("Profiles|website.com")
- if @user.read_only_attribute?(:location)
- = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) }
+ = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:location) }
- else
- = f.text_field :location
- = f.text_field :organization
- = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
+ = f.text_field :location, class: 'input-lg', placeholder: s_("Profiles|City, country")
+ = f.text_field :organization, class: 'input-md', help: s_("Profiles|Who you represent or work for")
+ = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
%hr
%h5= ("Private profile")
.checkbox-icon-inline-wrapper
@@ -118,7 +118,7 @@
%h5= s_("Profiles|Private contributions")
= f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
.help-block
- = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
+ = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
.prepend-top-default.append-bottom-default
= f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success'
= link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel'
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 94ec0cc5db8..d986c566928 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -25,7 +25,8 @@
- else
%p
- Download the Google Authenticator application from App Store or Google Play Store and scan this code.
+ Install a soft token authenticator like <a href="https://freeotp.github.io/">FreeOTP</a>
+ or Google Authenticator from your application repository and scan this QR code.
More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-4
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 6bf21570d41..31f1cf560e2 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,8 +1,8 @@
%div{ class: container_class }
- .nav-block.activity-filter-block.activities
+ .nav-block.d-none.d-sm-block.activities
= render 'shared/event_filter'
.controls
- = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do
+ = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn d-none d-sm-inline-block has-tooltip' do
= icon('rss')
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index e191b009db2..82b2ab64a5d 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,7 +2,7 @@
- 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-7.d-flex
+ .project-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.project-avatar.float-none
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64)
.d-flex.flex-column.flex-wrap.align-items-baseline
@@ -25,7 +25,7 @@
- if @project.has_extra_tags?
= _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown }
- .project-repo-buttons.col-md-12.col-lg-5.d-inline-flex.flex-wrap.justify-content-lg-end
+ .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'
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 0f709c65d0e..03ba1104507 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -18,17 +18,7 @@
Preview
%li.md-header-toolbar.active
- = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
- = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
- = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
- = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
- = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
- = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
- = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
- = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
- = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
- %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
- = sprite_icon("screen-full")
+ = render 'projects/blob/markdown_buttons', show_fullscreen_button: true
.md-write-holder
= yield
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 3c1f33ea95e..a54460f1196 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,4 +1,6 @@
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
+- file_name = params[:id].split("/").last ||= ""
+- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix{ data: { current_action: action } }
@@ -17,6 +19,8 @@
required: true, class: 'form-control new-file-name js-file-path-name-input'
.file-buttons
+ - if is_markdown
+ = render 'projects/blob/markdown_buttons', show_fullscreen_button: false
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/blob/_markdown_buttons.html.haml b/app/views/projects/blob/_markdown_buttons.html.haml
new file mode 100644
index 00000000000..1d6acd86108
--- /dev/null
+++ b/app/views/projects/blob/_markdown_buttons.html.haml
@@ -0,0 +1,13 @@
+.md-header-toolbar.active
+ = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
+ = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
+ = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
+ = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
+ = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
+ = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
+ = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
+ = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
+ = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
+ - if show_fullscreen_button
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
+ = sprite_icon("screen-full")
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index d453a3a9dac..159d9e44e17 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -1,16 +1,12 @@
- project = project || @project
.git-clone-holder.js-git-clone-holder.input-group
- - if allowed_protocols_present?
- .input-group-text.clone-dropdown-btn.btn
- %span.js-clone-dropdown-label
- = enabled_project_button(project, enabled_protocol)
- - else
- %a#clone-dropdown.input-group-text.btn.btn-primary.btn-xs.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
- %span.append-right-4.js-clone-dropdown-label
- = _('Clone')
- = sprite_icon("arrow-down", css_class: "icon")
- %form.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
+ %a#clone-dropdown.input-group-text.btn.btn-primary.btn-xs.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %span.append-right-4.js-clone-dropdown-label
+ = _('Clone')
+ = sprite_icon("arrow-down", css_class: "icon")
+ %ul.p-3.dropdown-menu.dropdown-menu-right.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options
+ - if ssh_enabled?
%li.pb-2
%label.label-bold
= _('Clone with SSH')
@@ -19,6 +15,7 @@
.input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
+ - if http_enabled?
%li
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index 778d27fc61d..cecc139b183 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -1,5 +1,3 @@
-- return unless Feature.enabled?(:project_cleanup, @project)
-
- expanded = Rails.env.test?
%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
diff --git a/app/views/projects/diffs/_render_error.html.haml b/app/views/projects/diffs/_render_error.html.haml
index c3dc47a56a7..7dbd9897e83 100644
--- a/app/views/projects/diffs/_render_error.html.haml
+++ b/app/views/projects/diffs/_render_error.html.haml
@@ -1,6 +1,2 @@
.nothing-here-block
- = _("This %{viewer} could not be displayed because %{reason}.") % { viewer: viewer.switcher_title, reason: diff_render_error_reason(viewer) }
-
- You can
- = diff_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
- instead.
+ = viewer.render_error_message.html_safe
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index b50b3ca207b..f048fb91304 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -55,7 +55,7 @@
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
- = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
@@ -88,4 +88,4 @@
%section.issuable-discussion
= render 'projects/issues/discussion'
-= render 'shared/issuable/sidebar', issuable: @issue
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
deleted file mode 100644
index a6e2565a485..00000000000
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
-= render "projects/merge_requests/mr_title"
-
-.merge-request-details.issuable-details
- = render "projects/merge_requests/mr_box"
-
-= render 'shared/issuable/sidebar', issuable: @merge_request
-
-#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
- resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
- .loading{ "v-if" => "isLoading" }
- %i.fa.fa-spinner.fa-spin
-
- .nothing-here-block{ "v-if" => "hasError" }
- {{conflictsData.errorMessage}}
-
- = render partial: "projects/merge_requests/conflicts/commit_stats"
-
- .files-wrapper{ "v-if" => "!isLoading && !hasError" }
- .files
- .diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
- .js-file-title.file-title
- %i.fa.fa-fw{ ":class" => "file.iconClass" }
- %strong {{file.filePath}}
- = render partial: 'projects/merge_requests/conflicts/file_actions'
- .diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
- = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
- .diff-wrap-lines.code.file-content.js-syntax-highlight{ "v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
- %parallel-conflict-lines{ ":file" => "file" }
- %div{ "v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'" }
- = render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
-
- = render partial: "projects/merge_requests/conflicts/submit_form"
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index a6e2565a485..09aeb81671a 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -6,7 +6,7 @@
.merge-request-details.issuable-details
= render "projects/merge_requests/mr_box"
-= render 'shared/issuable/sidebar', issuable: @merge_request
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index c178206dda4..d6f340d0ee2 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -5,6 +5,7 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
+- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
= render "projects/merge_requests/mr_title"
@@ -67,6 +68,7 @@
noteable_data: serialize_issuable(@merge_request),
noteable_type: 'MergeRequest',
target_type: 'merge_request',
+ help_page_path: suggest_changes_help_path,
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json} }
#commits.commits.tab-pane
@@ -76,6 +78,7 @@
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
#js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
+ help_page_path: suggest_changes_help_path,
current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json,
project_path: project_path(@merge_request.project),
changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg') } }
@@ -83,7 +86,8 @@
.mr-loading-status
= spinner
-= render 'shared/issuable/sidebar', issuable: @merge_request
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
+
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- if @merge_request.can_be_cherry_picked?
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
new file mode 100644
index 00000000000..f01d4e826b9
--- /dev/null
+++ b/app/views/projects/releases/index.html.haml
@@ -0,0 +1,5 @@
+- @no_container = true
+- page_title _('Releases')
+
+%div{ class: container_class }
+ #js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases') } }
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml
index 597c029f755..a1d74b91002 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_metrics.html.haml
@@ -1,6 +1,6 @@
- project = local_assigns.fetch(:project)
-.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
+.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } }
.card-header
= s_('PrometheusService|Common metrics')
%span.badge.badge-pill.js-monitored-count 0
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 1d0b0265bb7..9d4574c4590 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -4,7 +4,7 @@
= s_('PrometheusService|Metrics')
%p
= s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/metrics'), target: '_blank', rel: "noopener noreferrer"
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
= render 'projects/services/prometheus/metrics', project: @project
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 98e2829ba43..6966bf96724 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -43,13 +43,7 @@
%section.qa-variables-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
- %h4
- = _('Variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p.append-bottom-0
- = render "ci/variables/content"
+ = render 'ci/variables/header', expanded: expanded
.settings-content
= render 'ci/variables/index', save_endpoint: project_variables_path(@project)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 026bc44a05f..458096f9dd6 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -27,7 +27,7 @@
- if can?(current_user, :push_code, @project)
= link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn' do
= s_('TagsPage|New tag')
- = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn rss-btn has-tooltip' do
+ = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn d-none d-sm-inline-block has-tooltip' do
= icon("rss")
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/tags/releases/edit.html.haml
index 52c6c7ec424..52c6c7ec424 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/tags/releases/edit.html.haml
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index ab56f48ba4d..c4d52431d6e 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,4 +1,4 @@
-- if @search_objects.empty?
+- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
- else
.row-content-block
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index bd68a3e4c84..9a1db831ad3 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -8,15 +8,15 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
- = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
+ = link_to page_filter_path(sort: sort_value_due_date_soon) do
= sort_title_due_date_soon
- = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
+ = link_to page_filter_path(sort: sort_value_due_date_later) do
= sort_title_due_date_later
- = link_to page_filter_path(sort: sort_value_start_date_soon, label: true) do
+ = link_to page_filter_path(sort: sort_value_start_date_soon) do
= sort_title_start_date_soon
- = link_to page_filter_path(sort: sort_value_start_date_later, label: true) do
+ = link_to page_filter_path(sort: sort_value_start_date_later) do
= sort_title_start_date_later
- = link_to page_filter_path(sort: sort_value_name, label: true) do
+ = link_to page_filter_path(sort: sort_value_name) do
= sort_title_name_asc
- = link_to page_filter_path(sort: sort_value_name_desc, label: true) do
+ = link_to page_filter_path(sort: sort_value_name_desc) do
= sort_title_name_desc
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index b43662947a8..6e2527bd1a1 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -7,7 +7,9 @@
%button.btn.btn-primary.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
= sprite_icon("arrow-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
- %li
- = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }, default: true)
- %li
- = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
+ - if ssh_enabled?
+ %li
+ = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }, default: true)
+ - if http_enabled?
+ %li
+ = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' })
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 1618655182c..c6a391ae563 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -17,7 +17,7 @@
= render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
-- if Feature.enabled?(:issue_suggestions) && Feature.enabled?(:graphql)
+- if Gitlab::Graphql.enabled?
#js-suggestions{ data: { project_path: @project.full_path } }
= render 'shared/form_elements/description', model: issuable, form: form, project: project
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 157637dbd11..71123740ee4 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -4,20 +4,20 @@
%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
- = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
+ = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
- = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
+ = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 46634693067..20847378495 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,7 +11,7 @@
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
- .check-all-holder.hidden
+ .check-all-holder.d-none.d-sm-block.hidden
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9eecfa39390..0520eda37a4 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,32 +1,37 @@
-- todo = issuable_todo(issuable)
+-# `assignees` is being passed in for populating selected assignee values in the select box and rendering the assignee link
+ This should be removed when this sidebar is converted to Vue since assignee data is also available in the `issuable_sidebar` hash
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
- .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+- issuable_type = issuable_sidebar[:type]
+- signed_in = !!issuable_sidebar.dig(:current_user, :id)
+- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
+
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+ .issuable-sidebar
.block.issuable-sidebar-header
- - if current_user
+ - if signed_in
%span.issuable-header-text.hide-collapsed.float-left
= _('Todo')
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if current_user
- = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
+ - if signed_in
+ = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
- = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- - if current_user
+ = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
+ - if signed_in
.block.todo.hide-expanded
- = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
+ = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
.block.assignee.qa-assignee-block
- = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
+ = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- = render_if_exists 'shared/issuable/sidebar_item_epic', issuable: issuable
+ = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
+ - milestone = issuable_sidebar[:milestone] || {}
.block.milestone
- .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title.collapse-truncated-title
- - if issuable.milestone
- = issuable.milestone.title
+ - if milestone.present?
+ = milestone[:title]
- else
= _('None')
.title.hide-collapsed
@@ -35,49 +40,50 @@
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- - if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
+ - if milestone.present?
+ = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
- = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
- = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }})
- - if issuable.has_attribute?(:time_estimate)
- #issuable-time-tracker.block
- // Fallback while content is loading
- .title.hide-collapsed
- = _('Time tracking')
- = icon('spinner spin', 'aria-hidden': 'true')
- - if issuable.has_attribute?(:due_date)
+ = f.hidden_field 'milestone_id', value: milestone[:id], id: nil
+ = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
+
+ #issuable-time-tracker.block
+ // Fallback while content is loading
+ .title.hide-collapsed
+ = _('Time tracking')
+ = icon('spinner spin', 'aria-hidden': 'true')
+
+ - if issuable_sidebar.has_key?(:due_date)
.block.due_date
- .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable) }
+ .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
- = issuable.due_date.try(:to_s, :medium) || 'None'
+ = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
.title.hide-collapsed
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ - if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
%span.value-content
- - if issuable.due_date
- %span.bold= issuable.due_date.to_s(:medium)
+ - if issuable_sidebar[:due_date]
+ %span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else
%span.no-value
= _('No due date')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) }
+ - if can_edit_issuable
+ %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
= _('remove due date')
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ - if can_edit_issuable
.selectbox.hide-collapsed
- = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
+ = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd')
.dropdown
- %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } }
+ %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
= icon('chevron-down', 'aria-hidden': 'true')
@@ -86,56 +92,56 @@
= dropdown_content do
.js-due-date-calendar
- - if @labels
- - selected_labels = issuable.labels
- .block.labels
- .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } }
- = icon('tags', 'aria-hidden': 'true')
- %span
- = selected_labels.size
- .title.hide-collapsed
- = _('Labels')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- - if selected_labels.any?
- - selected_labels.each do |label|
- = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
- - else
- %span.no-value
- = _('None')
- .selectbox.hide-collapsed
+ - selected_labels = issuable_sidebar[:labels]
+ .block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
+ = icon('tags', 'aria-hidden': 'true')
+ %span
+ = selected_labels.size
+ .title.hide-collapsed
+ = _('Labels')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
+ - if can_edit_issuable
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
+ - if selected_labels.any?
- selected_labels.each do |label|
- = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } }
- %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
- = multi_label_name(selected_labels, "Labels")
- = icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default"
- - if can? current_user, :admin_label, @project and @project
- = render partial: "shared/issuable/label_page_create"
-
- = render_if_exists 'shared/issuable/sidebar_weight', issuable: issuable
-
- - if issuable.has_attribute?(:confidential)
+ = link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do
+ %span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } }
+ = label[:title]
+ - else
+ %span.no-value
+ = _('None')
+ .selectbox.hide-collapsed
+ - selected_labels.each do |label|
+ = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable_type}[label_names][]", ability_name: issuable_type, show_no: "true", show_any: "true", namespace_path: issuable_sidebar[:namespace_path], project_path: issuable_sidebar[:project_path], issue_update: issuable_sidebar[:issuable_json_path], labels: issuable_sidebar[:project_labels_path], display: 'static' } }
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
+ = multi_label_name(selected_labels, "Labels")
+ = icon('chevron-down', 'aria-hidden': 'true')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default"
+ - if issuable_sidebar.dig(:current_user, :can_admin_label)
+ = render partial: "shared/issuable/label_page_create"
+
+ = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar
+
+ - if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript
- %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
+ %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- - if issuable.has_attribute?(:discussion_locked)
- -# haml-lint:disable InlineJavaScript
- %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
- #js-lock-entry-point
+ -# haml-lint:disable InlineJavaScript
+ %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
+ #js-lock-entry-point
.js-sidebar-participants-entry-point
- - if current_user
+ - if signed_in
.js-sidebar-subscriptions-entry-point
- - project_ref = cross_project_reference(@project, issuable)
+ - project_ref = issuable_sidebar[:reference]
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
@@ -145,7 +151,8 @@
%cite{ title: project_ref }
= project_ref
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
- - if current_user && issuable.can_move?(current_user)
+
+ - if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= custom_icon('icon_arrow_right')
@@ -164,4 +171,4 @@
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
-# haml-lint:disable InlineJavaScript
- %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
+ %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 8a13c7a3b83..c5cce1823f0 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,12 +1,17 @@
-- if issuable.is_a?(Issue)
- #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]", signed_in: signed_in } }
+- issuable_type = issuable_sidebar[:type]
+- signed_in = !!issuable_sidebar.dig(:current_user, :id)
+- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
+
+- if issuable_type == "issue"
+ #js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
= icon('spinner spin')
- else
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) }
- - if issuable.assignee
- = link_to_member(@project, issuable.assignee, size: 24)
+ - assignee = assignees.first
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
+ - if issuable_sidebar[:assignee]
+ = link_to_member(@project, assignee, size: 24)
- else
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
@@ -18,13 +23,13 @@
%a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon
.value.hide-collapsed
- - if issuable.assignee
- = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- - if !issuable.can_be_merged_by?(issuable.assignee)
+ - if issuable_sidebar[:assignee]
+ = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
+ - if issuable_sidebar[:assignee][:can_merge]
%span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
- = issuable.assignee.to_reference
+ @#{issuable_sidebar[:assignee][:username]}
- else
%span.assign-yourself.no-value
= _('No assignee')
@@ -34,19 +39,33 @@
= _('assign yourself')
.selectbox.hide-collapsed
- - issuable.assignees.each do |assignee|
- = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
+ - if assignees.none?
+ = hidden_field_tag "#{issuable_type}[assignee_ids][]", 0, id: nil
+ - else
+ - assignees.each do |assignee|
+ = hidden_field_tag "#{issuable_type}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
- - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } }
+ - options = { toggle_class: 'js-user-search js-author-search',
+ title: _('Assign to'),
+ filter: true,
+ dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author',
+ placeholder: _('Search users'),
+ data: { first_user: issuable_sidebar.dig(:current_user, :username),
+ current_user: true,
+ project_id: issuable_sidebar[:project_id],
+ author_id: issuable_sidebar[:author_id],
+ field_name: "#{issuable_type}[assignee_ids][]",
+ issue_update: issuable_sidebar[:issuable_json_path],
+ ability_name: issuable_type,
+ null_user: true,
+ display: 'static' } }
- title = _('Select assignee')
- - if issuable.is_a?(Issue)
- - unless issuable.assignees.any?
- = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil
+ - if issuable_type == "issue"
- dropdown_options = issue_assignees_dropdown_options
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" }
+ - data = { field_name: "#{issuable_type}[assignee_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index 660ee6d5777..de4df016cfb 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,15 +1,15 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : _('Mark todo as done')
-- todo_content = is_collapsed ? sprite_icon('todo-add') : _('Add todo')
+- has_todo = !!issuable_sidebar.dig(:current_user, :todo, :id)
+
+- todo_button_data = issuable_todo_button_data(issuable_sidebar, is_collapsed)
+- button_title = has_todo ? todo_button_data[:mark_text] : todo_button_data[:todo_text]
+- button_icon = has_todo ? todo_button_data[:mark_icon] : todo_button_data[:todo_icon]
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
- title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
- 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
- data: issuable_todo_button_data(issuable, todo, is_collapsed) }
+ title: button_title,
+ 'aria-label' => button_title,
+ data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner<
- - if todo
- = mark_content
- - else
- = todo_content
+ = is_collapsed ? button_icon : button_title
= icon('spin spinner', 'aria-hidden': 'true')
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index c211b9fcaa2..b6ea9185b10 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -10,11 +10,12 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
- = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sort_title)
- = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sort_title)
- = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sort_title)
- = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone, label: true), sort_title)
- = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date, label: true), sort_title) if viewing_issues
- = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity, label: true), sort_title)
- = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority, label: true), sort_title)
+ = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)
+ = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sort_title)
+ = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated), sort_title)
+ = sortable_item(sort_title_milestone, page_filter_path(sort: sort_value_milestone), sort_title)
+ = sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
+ = sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
+ = sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
+ = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index ac8d58c0bfe..e370dff9526 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -19,10 +19,9 @@
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group.row
- - has_labels = @labels && @labels.any?
= form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
= form.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .col-sm-10{ class: "#{"col-md-8" if has_due_date}" }
.issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
diff --git a/app/views/shared/issuable/nav_links/_all.html.haml b/app/views/shared/issuable/nav_links/_all.html.haml
index d7ad7090a45..c92a50bcb70 100644
--- a/app/views/shared/issuable/nav_links/_all.html.haml
+++ b/app/views/shared/issuable/nav_links/_all.html.haml
@@ -2,5 +2,5 @@
- counter = local_assigns.fetch(:counter)
%li{ class: active_when(params[:state] == 'all') }>
- = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
+ = link_to page_filter_path(state: 'all'), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
#{counter}
diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
index d664ef1cc2f..07e96eea062 100644
--- a/app/views/shared/labels/_sort_dropdown.html.haml
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -6,4 +6,4 @@
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
- label_sort_options_hash.each do |value, title|
- = sortable_item(title, page_filter_path(sort: value, label: true, subscribed: params[:subscribed]), sort_title)
+ = sortable_item(title, page_filter_path(sort: value), sort_title)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index ed7fefba56d..40b8374848e 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,5 +1,5 @@
- dashboard = local_assigns[:dashboard]
-- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
+- custom_dom_id = dom_id(milestone.try(:milestone) ? milestone.milestone : milestone)
- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
@@ -21,10 +21,12 @@
= milestone.group.full_name
- if milestone.legacy_group_milestone?
.projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
- = dashboard ? milestone.project.full_name : milestone.project.name
+ - link_to milestone_path(milestone.milestone) do
+ %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
+ = dashboard ? milestone.project.full_name : milestone.project.name
+ - if milestone.project
+ .label-badge.label-badge-gray.d-inline-block
+ = milestone.project.full_name
.col-sm-4.milestone-progress
= milestone_progress_bar(milestone)
@@ -58,5 +60,5 @@
- else
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
- if dashboard
- .status-box.status-box-milestone
+ .label-badge.label-badge-gray
= milestone_type
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index becd1c4884e..b24075c7849 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -65,7 +65,7 @@
%span.bold= milestone.due_date.to_s(:medium)
- else
%span.no-value No due date
- - remaining_days = remaining_days_in_words(milestone)
+ - remaining_days = remaining_days_in_words(milestone.due_date, milestone.start_date)
- if remaining_days.present?
= surround '(', ')' do
%span.remaining-days= remaining_days
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 0499b04a482..55b1c14022f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -62,20 +62,19 @@
%th Open issues
%th State
%th Due date
- - milestone.milestones.each do |ms|
%tr
%td
- - project_name = group ? ms.project.name : ms.project.full_name
- = link_to project_name, project_milestone_path(ms.project, ms)
+ - project_name = group ? milestone.project.name : milestone.project.full_name
+ = link_to project_name, milestone_path(milestone.milestone)
%td
- = ms.issues_visible_to_user(current_user).opened.count
+ = milestone.milestone.issues_visible_to_user(current_user).opened.count
%td
- - if ms.closed?
+ - if milestone.closed?
Closed
- else
Open
%td
- = ms.expires_at
+ = milestone.expires_at
- elsif milestone.group_milestone?
%br
View
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 9dde77fccef..fea7e17be3d 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -72,13 +72,13 @@
title: _('Forks'), data: { container: 'body', placement: 'top' } do
= sprite_icon('fork', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.forks_count)
- - if show_merge_request_count?(merge_requests, compact_mode)
+ - if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project),
class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip",
title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_merge_requests_count)
- - if show_issue_count?(issues, compact_mode)
+ - if show_issue_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project),
class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip",
title: _('Issues'), data: { container: 'body', placement: 'top' } do
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 10bfc30492a..a43296aa806 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -30,7 +30,7 @@
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true)
- - if public_snippet?
+ - if @snippet.embeddable?
.embed-snippet
.input-group
.input-group-prepend
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index b5bc1180290..d22905ecc93 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -9,22 +9,24 @@
.col-md-12.col-lg-6
- if can?(current_user, :read_cross_project)
.activities-block
+ .append-right-5
+ .prepend-top-16
+ .d-flex.align-items-center.border-bottom
+ %h4.flex-grow
+ = s_('UserProfile|Activity')
+ = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
+ .overview-content-list{ data: { href: user_path } }
+ .center.light.loading
+ = spinner nil, true
+
+ .col-md-12.col-lg-6
+ .projects-block
+ .prepend-left-5
.prepend-top-16
.d-flex.align-items-center.border-bottom
%h4.flex-grow
- = s_('UserProfile|Activity')
- = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_path } }
+ = s_('UserProfile|Personal projects')
+ = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
+ .overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
= spinner nil, true
-
- .col-md-12.col-lg-6
- .projects-block
- .prepend-top-16
- .d-flex.align-items-center.border-bottom
- %h4.flex-grow
- = s_('UserProfile|Personal projects')
- = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_projects_path } }
- .center.light.loading
- = spinner nil, true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index dd2cd36eac2..8da63a29ca6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -71,7 +71,7 @@
= icon('twitter-square')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider
- = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'noopener noreferrer nofollow'
+ = 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')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index bc26b3f8ef2..d3cf21db335 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -27,7 +27,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:cluster_wait_for_ingress_ip_address
-- gcp_cluster:cluster_platform_configure
+- gcp_cluster:cluster_configure
- gcp_cluster:cluster_project_configure
- github_import_advance_stage
@@ -88,6 +88,7 @@
- object_pool:object_pool_create
- object_pool:object_pool_schedule_join
- object_pool:object_pool_join
+- object_pool:object_pool_destroy
- default
- mailers # ActionMailer::DeliveryJob.queue_name
diff --git a/app/workers/cluster_platform_configure_worker.rb b/app/workers/cluster_configure_worker.rb
index aa7570caa79..63e6cc147be 100644
--- a/app/workers/cluster_platform_configure_worker.rb
+++ b/app/workers/cluster_configure_worker.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class ClusterPlatformConfigureWorker
+class ClusterConfigureWorker
include ApplicationWorker
include ClusterQueue
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 3d5894b73ec..926ae2b7286 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -10,7 +10,7 @@ class ClusterProvisionWorker
Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
end
- ClusterPlatformConfigureWorker.perform_async(cluster.id) if cluster.user?
+ ClusterConfigureWorker.perform_async(cluster.id) if cluster.user?
end
end
end
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 4726e416182..c8ccaf0c487 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -8,14 +8,35 @@ module MailScheduler
include MailSchedulerQueue
def perform(meth, *args)
- deserialized_args = ActiveJob::Arguments.deserialize(args)
+ check_arguments!(args)
+ deserialized_args = ActiveJob::Arguments.deserialize(args)
notification_service.public_send(meth, *deserialized_args) # rubocop:disable GitlabSecurity/PublicSend
rescue ActiveJob::DeserializationError
+ # No-op.
+ # This exception gets raised when an argument
+ # is correct (deserializeable), but it still cannot be deserialized.
+ # This can happen when an object has been deleted after
+ # rails passes this job to sidekiq, but before
+ # sidekiq gets it for execution.
+ # In this case just do nothing.
end
def self.perform_async(*args)
super(*ActiveJob::Arguments.serialize(args))
end
+
+ private
+
+ # If an argument is in the ActiveJob::Arguments::TYPE_WHITELIST list,
+ # it means the argument cannot be deserialized.
+ # Which means there's something wrong with our code.
+ def check_arguments!(args)
+ args.each do |arg|
+ if arg.class.in?(ActiveJob::Arguments::TYPE_WHITELIST)
+ raise(ArgumentError, "Argument `#{arg}` cannot be deserialized because of its type")
+ end
+ end
+ end
end
end
diff --git a/app/workers/object_pool/destroy_worker.rb b/app/workers/object_pool/destroy_worker.rb
new file mode 100644
index 00000000000..ca00d467d9b
--- /dev/null
+++ b/app/workers/object_pool/destroy_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ObjectPool
+ class DestroyWorker
+ include ApplicationWorker
+ include ObjectPoolQueue
+
+ def perform(pool_repository_id)
+ pool = PoolRepository.find_by_id(pool_repository_id)
+ return unless pool&.obsolete?
+
+ pool.delete_object_pool
+ pool.destroy
+ end
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 72a1733a2a8..bbd4ab159e4 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,7 +3,7 @@
class PostReceive
include ApplicationWorker
- def perform(gl_repository, identifier, changes)
+ def perform(gl_repository, identifier, changes, push_options = [])
project, is_wiki = Gitlab::GlRepository.parse(gl_repository)
if project.nil?
@@ -15,7 +15,7 @@ class PostReceive
# Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's.
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
- post_received = Gitlab::GitPostReceive.new(project, identifier, changes)
+ post_received = Gitlab::GitPostReceive.new(project, identifier, changes, push_options)
if is_wiki
process_wiki_changes(post_received)
@@ -38,9 +38,21 @@ class PostReceive
post_received.changes_refs do |oldrev, newrev, ref|
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitTagPushService.new(
+ post_received.project,
+ @user,
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref,
+ push_options: post_received.push_options).execute
elsif Gitlab::Git.branch_ref?(ref)
- GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
+ GitPushService.new(
+ post_received.project,
+ @user,
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref,
+ push_options: post_received.push_options).execute
end
changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref)
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index 98c81956cba..f34ed6c4844 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -4,6 +4,10 @@ class StuckMergeJobsWorker
include ApplicationWorker
include CronjobQueue
+ def self.logger
+ Rails.logger
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def perform
stuck_merge_requests.find_in_batches(batch_size: 100) do |group|
@@ -35,7 +39,7 @@ class StuckMergeJobsWorker
# We rely on state machine callbacks to update head_pipeline_id
merge_requests_to_reopen.each(&:unlock_mr)
- Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
+ self.class.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end
# rubocop: enable CodeReuse/ActiveRecord