summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLuke Bennett <lukeeeebennettplus@gmail.com>2018-04-05 18:45:35 +0100
committerLuke Bennett <lukeeeebennettplus@gmail.com>2018-04-05 18:45:35 +0100
commit8131a02fef9241f396c827cb7613ddd307f0a551 (patch)
treeeca9cac2c0ba6e228b42bab90d8fd0cf0a9b191e /app
parentee1954efc19470c477ddbdf965731dc25598e6b0 (diff)
parentf103475766fecc6e6fdf996e9cfaaa41e795962f (diff)
downloadgitlab-ce-8131a02fef9241f396c827cb7613ddd307f0a551.tar.gz
Merge remote-tracking branch 'origin/master' into deprecation-warning-for-dynamic-milestones
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_canceled.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_created.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_failed.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_manual.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_not_found.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_pending.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_running.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_skipped.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_success.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/canary/favicon_status_warning.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/favicon-yellow.icobin0 -> 5430 bytes
-rw-r--r--app/assets/javascripts/api.js146
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js2
-rw-r--r--app/assets/javascripts/boards/services/board_service.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js18
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue34
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue56
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide.vue87
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue83
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue23
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue97
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue27
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue61
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue106
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue87
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue109
-rw-r--r--app/assets/javascripts/ide/ide_router.js66
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js36
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js21
-rw-r--r--app/assets/javascripts/ide/lib/editor.js44
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js13
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js125
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js84
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js98
-rw-r--r--app/assets/javascripts/ide/stores/getters.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js67
-rw-r--r--app/assets/javascripts/ide/stores/mutations/merge_request.js33
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js32
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js23
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue24
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue27
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js1
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js10
-rw-r--r--app/assets/javascripts/milestone_select.js8
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue291
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue159
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue413
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue48
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue226
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue228
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue60
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue24
-rw-r--r--app/assets/javascripts/notes.js10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue10
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/constants.js1
-rw-r--r--app/assets/javascripts/notes/index.js5
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js1
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue8
-rw-r--r--app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js2
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js (renamed from app/assets/javascripts/pages/ci/lints/ci_lint_editor.js)0
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/new/index.js (renamed from app/assets/javascripts/pages/ci/lints/new/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/show/index.js (renamed from app/assets/javascripts/pages/ci/lints/show/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue17
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue101
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue53
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue66
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue114
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue (renamed from app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js)76
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue97
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue90
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss113
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/lint.scss21
-rw-r--r--app/assets/stylesheets/pages/note_form.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss16
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss22
-rw-r--r--app/assets/stylesheets/pages/repo.scss91
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/controllers/admin/appearances_controller.rb18
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/ci/lints_controller.rb15
-rw-r--r--app/controllers/concerns/group_tree.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb6
-rw-r--r--app/controllers/concerns/notes_actions.rb2
-rw-r--r--app/controllers/concerns/send_file_upload.rb17
-rw-r--r--app/controllers/concerns/uploads_actions.rb30
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/profiles_controller.rb6
-rw-r--r--app/controllers/projects/artifacts_controller.rb12
-rw-r--r--app/controllers/projects/branches_controller.rb10
-rw-r--r--app/controllers/projects/ci/lints_controller.rb27
-rw-r--r--app/controllers/projects/discussions_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb32
-rw-r--r--app/controllers/projects/labels_controller.rb5
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb56
-rw-r--r--app/controllers/projects/milestones_controller.rb10
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb37
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/protected_branches_controller.rb8
-rw-r--r--app/controllers/projects/protected_refs_controller.rb14
-rw-r--r--app/controllers/projects/protected_tags_controller.rb8
-rw-r--r--app/controllers/projects/raw_controller.rb3
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb56
-rw-r--r--app/controllers/projects/settings/repository_controller.rb4
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/finders/admin/projects_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/labels_finder.rb35
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/appearances_helper.rb16
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/boards_helper.rb6
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb10
-rw-r--r--app/helpers/namespaces_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb18
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/helpers/preferences_helper.rb14
-rw-r--r--app/helpers/services_helper.rb25
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb9
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/ci/artifact_blob.rb7
-rw-r--r--app/models/ci/build.rb160
-rw-r--r--app/models/ci/build_metadata.rb35
-rw-r--r--app/models/ci/group_variable.rb2
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/ci/pipeline.rb26
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/clusters/cluster.rb6
-rw-r--r--app/models/clusters/concerns/application_status.rb2
-rw-r--r--app/models/commit.rb5
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/avatarable.rb1
-rw-r--r--app/models/concerns/chronic_duration_attribute.rb39
-rw-r--r--app/models/concerns/deployment_platform.rb22
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/deploy_key.rb4
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/lfs_object.rb15
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/merge_request.rb27
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/note.rb15
-rw-r--r--app/models/notification_recipient.rb15
-rw-r--r--app/models/notification_setting.rb7
-rw-r--r--app/models/project.rb69
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/gemnasium_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/redirect_route.rb28
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/route.rb26
-rw-r--r--app/models/service.rb9
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/upload.rb19
-rw-r--r--app/models/user.rb28
-rw-r--r--app/policies/issuable_policy.rb14
-rw-r--r--app/policies/issue_policy.rb2
-rw-r--r--app/policies/merge_request_policy.rb1
-rw-r--r--app/policies/project_policy.rb35
-rw-r--r--app/policies/protected_branch_policy.rb9
-rw-r--r--app/presenters/ci/build_metadata_presenter.rb18
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/build_metadata_entity.rb6
-rw-r--r--app/serializers/discussion_entity.rb6
-rw-r--r--app/serializers/note_entity.rb30
-rw-r--r--app/serializers/note_serializer.rb3
-rw-r--r--app/serializers/project_note_entity.rb25
-rw-r--r--app/serializers/project_note_serializer.rb3
-rw-r--r--app/serializers/status_entity.rb10
-rw-r--r--app/services/boards/issues/move_service.rb5
-rw-r--r--app/services/boards/list_service.rb6
-rw-r--r--app/services/boards/lists/create_service.rb8
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/create_pipeline_stages_service.rb20
-rw-r--r--app/services/ci/pipeline_trigger_service.rb12
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/issues/update_service.rb17
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/notes/post_process_service.rb10
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/projects/autocomplete_service.rb3
-rw-r--r--app/services/projects/create_service.rb23
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb5
-rw-r--r--app/services/projects/import_export/export_service.rb37
-rw-r--r--app/services/projects/import_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb40
-rw-r--r--app/services/protected_branches/create_service.rb17
-rw-r--r--app/services/protected_branches/destroy_service.rb9
-rw-r--r--app/services/protected_branches/update_service.rb2
-rw-r--r--app/services/protected_tags/destroy_service.rb7
-rw-r--r--app/services/quick_actions/interpret_service.rb5
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/verify_pages_domain_service.rb10
-rw-r--r--app/uploaders/attachment_uploader.rb6
-rw-r--r--app/uploaders/avatar_uploader.rb8
-rw-r--r--app/uploaders/file_mover.rb9
-rw-r--r--app/uploaders/file_uploader.rb56
-rw-r--r--app/uploaders/gitlab_uploader.rb10
-rw-r--r--app/uploaders/job_artifact_uploader.rb9
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb1
-rw-r--r--app/uploaders/lfs_object_uploader.rb6
-rw-r--r--app/uploaders/namespace_file_uploader.rb11
-rw-r--r--app/uploaders/object_storage.rb442
-rw-r--r--app/uploaders/personal_file_uploader.rb17
-rw-r--r--app/uploaders/records_uploads.rb3
-rw-r--r--app/validators/certificate_fingerprint_validator.rb9
-rw-r--r--app/validators/importable_url_validator.rb6
-rw-r--r--app/validators/top_level_group_validator.rb7
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml12
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml39
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml30
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml47
-rw-r--r--app/views/admin/application_settings/_email.html.haml26
-rw-r--r--app/views/admin/application_settings/_form.html.haml873
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml27
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml22
-rw-r--r--app/views/admin/application_settings/_influx.html.haml68
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml54
-rw-r--r--app/views/admin/application_settings/_koding.html.haml24
-rw-r--r--app/views/admin/application_settings/_logging.html.haml36
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml12
-rw-r--r--app/views/admin/application_settings/_pages.html.haml22
-rw-r--r--app/views/admin/application_settings/_performance.html.haml19
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml16
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml20
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml28
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml19
-rw-r--r--app/views/admin/application_settings/_registry.html.haml10
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml62
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml58
-rw-r--r--app/views/admin/application_settings/_signin.html.haml59
-rw-r--r--app/views/admin/application_settings/_signup.html.haml58
-rw-r--r--app/views/admin/application_settings/_spam.html.haml65
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml13
-rw-r--r--app/views/admin/application_settings/_usage.html.haml37
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml66
-rw-r--r--app/views/admin/application_settings/show.html.haml305
-rw-r--r--app/views/admin/projects/show.html.haml10
-rw-r--r--app/views/ci/lints/show.html.haml36
-rw-r--r--app/views/ci/variables/_variable_row.html.haml4
-rw-r--r--app/views/email_rejection_mailer/rejection.text.haml3
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/import/github/new.html.haml3
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/devise_empty.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml16
-rw-r--r--app/views/notify/push_to_merge_request_email.html.haml26
-rw-r--r--app/views/notify/push_to_merge_request_email.text.haml13
-rw-r--r--app/views/projects/_export.html.haml4
-rw-r--r--app/views/projects/ci/lints/_create.html.haml (renamed from app/views/ci/lints/_create.html.haml)0
-rw-r--r--app/views/projects/ci/lints/show.html.haml27
-rw-r--r--app/views/projects/clusters/show.html.haml4
-rw-r--r--app/views/projects/clusters/user/_header.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commits/_commit.atom.builder2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/edit.html.haml8
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml1
-rw-r--r--app/views/projects/new.html.haml8
-rw-r--r--app/views/projects/no_repo.html.haml6
-rw-r--r--app/views/projects/pages_domains/new.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml4
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml (renamed from app/views/projects/pipelines_settings/_badge.html.haml)0
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml (renamed from app/views/projects/pipelines_settings/_show.html.haml)14
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml13
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml3
-rw-r--r--app/views/shared/_label.html.haml1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_milestone.html.haml1
-rw-r--r--app/views/shared/milestones/_top.html.haml6
-rw-r--r--app/views/shared/notes/_form.html.haml35
-rw-r--r--app/views/shared/web_hooks/_form.html.haml7
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/concerns/object_storage_queue.rb8
-rw-r--r--app/workers/git_garbage_collect_worker.rb9
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/background_move_worker.rb29
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb214
-rw-r--r--app/workers/object_storage_upload_worker.rb21
-rw-r--r--app/workers/project_export_worker.rb14
-rw-r--r--app/workers/repository_fork_worker.rb43
341 files changed, 6856 insertions, 3378 deletions
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico
new file mode 100644
index 00000000000..48b1095370d
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_created.ico b/app/assets/images/ci_favicons/canary/favicon_status_created.ico
new file mode 100644
index 00000000000..623c728faf6
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_failed.ico b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico
new file mode 100644
index 00000000000..3073fe5a761
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_manual.ico b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico
new file mode 100644
index 00000000000..6c713d7b675
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico
new file mode 100644
index 00000000000..dbf855fdafd
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_pending.ico b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico
new file mode 100644
index 00000000000..ccd00606aeb
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_running.ico b/app/assets/images/ci_favicons/canary/favicon_status_running.ico
new file mode 100644
index 00000000000..968e7c4c2d4
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico
new file mode 100644
index 00000000000..7e3be35cc3a
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_success.ico b/app/assets/images/ci_favicons/canary/favicon_status_success.ico
new file mode 100644
index 00000000000..a1fb6e91d65
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_warning.ico b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico
new file mode 100644
index 00000000000..5d931619fb2
--- /dev/null
+++ b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
new file mode 100644
index 00000000000..b650f277fb6
--- /dev/null
+++ b/app/assets/images/favicon-yellow.ico
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f..8ad3d18b302 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
+ mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+ mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+ mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
- const url = Api.buildUrl(Api.groupPath)
- .replace(':id', groupId);
- return axios.get(url)
- .then(({ data }) => {
- callback(data);
+ const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
- return data;
- });
+ return data;
+ });
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
- return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
+ })
.then(({ data }) => {
callback(data);
@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
- return axios.get(url, {
- params: Object.assign(defaults, options),
- })
+ return axios
+ .get(url, {
+ params: Object.assign(defaults, options),
+ })
.then(({ data }) => {
callback(data);
@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
- const url = Api.buildUrl(Api.projectPath)
- .replace(':id', encodeURIComponent(projectPath));
+ const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
+ // Return Merge Request for project
+ mergeRequest(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestChanges(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestChangesPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
+
+ return axios.get(url);
+ },
+
+ mergeRequestVersions(projectPath, mergeRequestId) {
+ const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':mrid', mergeRequestId);
return axios.get(url);
},
@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
- return axios.post(url, {
- label: data,
- })
+ return axios
+ .post(url, {
+ label: data,
+ })
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
- const url = Api.buildUrl(Api.groupProjectsPath)
- .replace(':id', groupId);
- return axios.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
- })
+ const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ return axios
+ .get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ },
+ })
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const url = Api.buildUrl(Api.commitPath)
- .replace(':id', encodeURIComponent(id));
+ const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
- .replace(':branch', branch);
+ .replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
- const url = Api.buildUrl(Api.licensePath)
- .replace(':key', key);
- return axios.get(url, {
- params: data,
- })
+ const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ return axios
+ .get(url, {
+ params: data,
+ })
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
- const url = Api.buildUrl(Api.gitignorePath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
- const url = Api.buildUrl(Api.gitlabCiYmlPath)
- .replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
- return axios.get(url)
- .then(({ data }) => callback(data));
+ return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
- return axios.get(url)
+ return axios
+ .get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign({
- search: query,
- per_page: 20,
- }, options),
+ params: Object.assign(
+ {
+ search: query,
+ per_page: 20,
+ },
+ options,
+ ),
});
},
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 6da33a26e58..0e1ca7fe883 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
-import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
+import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -300,7 +300,7 @@ class AwardsHandler {
}
isInVueNoteablePage() {
- return isInIssuePage() || this.isVueMRDiscussions();
+ return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
}
getVotesBlock() {
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7dcf1aeed17..eb4e59d12b1 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -31,7 +31,7 @@ export default function renderMath($els) {
if (!$els.length) return;
Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'),
- import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
+ import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]).then(([katex]) => {
renderWithKaTeX($els, katex);
}).catch(() => flash(__('An error occurred while rendering KaTeX')));
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index d78d4701974..7c90597f77c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,7 @@ export default class BoardService {
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
all() {
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 745f3404295..e177a3bfdc7 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -33,7 +33,7 @@ export default class VariableList {
selector: '.js-ci-variable-input-key',
default: '',
},
- value: {
+ secret_value: {
selector: '.js-ci-variable-input-value',
default: '',
},
@@ -105,7 +105,7 @@ export default class VariableList {
setupToggleButtons($row[0]);
// Reset the resizable textarea
- $row.find(this.inputMap.value.selector).css('height', '');
+ $row.find(this.inputMap.secret_value.selector).css('height', '');
const $environmentSelect = $row.find('.js-variable-environment-toggle');
if ($environmentSelect.length) {
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 46232726510..d62d3c23654 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -1,4 +1,5 @@
// ECMAScript polyfills
+import 'core-js/fn/array/fill';
import 'core-js/fn/array/find';
import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index e6390f0855b..d7e1de18d09 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
this.groupsOnly = isGroup;
- this.groupAncestor = isGroupAncestor;
- this.isGroupDecendent = isGroupDecendent;
+ this.includeAncestorGroups = isGroupAncestor;
+ this.includeDescendantGroups = isGroupDecendent;
this.setupMapping();
@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
}
getLabelsEndpoint() {
- const endpoint = `${this.baseEndpoint}/labels.json`;
+ let endpoint = `${this.baseEndpoint}/labels.json?`;
+
+ if (this.groupsOnly) {
+ endpoint = `${endpoint}only_group_labels=true&`;
+ }
+
+ if (this.includeAncestorGroups) {
+ endpoint = `${endpoint}include_ancestor_groups=true&`;
+ }
+
+ if (this.includeDescendantGroups) {
+ endpoint = `${endpoint}include_descendant_groups=true`;
+ }
return endpoint;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 71b7e80335b..cf5ba1e1771 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
constructor({
page,
isGroup = false,
- isGroupAncestor = false,
+ isGroupAncestor = true,
isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
+ isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8259133c95b..7e9770a9ea2 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -54,6 +54,7 @@ class GfmAutoComplete {
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
+ skipMarkdownCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
displayTpl(value) {
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
@@ -376,15 +377,23 @@ class GfmAutoComplete {
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
},
beforeInsert(value) {
- let resultantValue = value;
+ let withoutAt = value.substring(1);
+ const at = value.charAt();
+
if (value && !this.setting.skipSpecialCharacterTest) {
- const withoutAt = value.substring(1);
- const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ const regex = at === '~' ? /\W|^\d+$/ : /\W/;
if (withoutAt && regex.test(withoutAt)) {
- resultantValue = `${value.charAt()}"${withoutAt}"`;
+ withoutAt = `"${withoutAt}"`;
}
}
- return resultantValue;
+
+ // We can ignore this for quick actions because they are processed
+ // before Markdown.
+ if (!this.setting.skipMarkdownCharacterTest) {
+ withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
+ }
+
+ return `${at}${withoutAt}`;
},
matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 86b34a6e360..fa48d7d1915 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -753,7 +753,7 @@ GitLabDropdown = (function() {
}
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return;
+ return [selectedObject];
}
if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51..037e3efb4ce 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
+import icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- props: {
- file: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ changedIcon() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- changedIconClass() {
- return `multi-${this.changedIcon}`;
- },
+ changedIconClass() {
+ return `multi-${this.changedIcon}`;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 18934af004a..560cdd941cd 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,38 +1,36 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import router from '../../ide_router';
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- props: {
- file: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ iconName() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
},
- computed: {
- iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
- },
+ iconClass() {
+ return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
- methods: {
- ...mapActions([
- 'discardFileChanges',
- 'updateViewer',
- ]),
- openFileInEditor(file) {
- this.updateViewer('diff');
-
- router.push(`/project${file.url}`);
- },
+ },
+ methods: {
+ ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
+ openFileInEditor(file) {
+ return this.openPendingTab(file).then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ });
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0..0c44a755f56 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
- export default {
- components: {
- Icon,
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewer: {
- type: String,
- required: true,
- },
- showShadow: {
- type: Boolean,
- required: true,
- },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- changeMode(mode) {
- this.$emit('click', mode);
- },
+ viewer: {
+ type: String,
+ required: true,
},
- };
+ showShadow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ mergeReviewLine() {
+ return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
+ },
+ methods: {
+ changeMode(mode) {
+ this.$emit('click', mode);
+ },
+ },
+};
</script>
<template>
@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === 'mrdiff' && mergeRequestId">
+ {{ mergeReviewLine }}
+ </template>
+ <template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
+ <template v-if="mergeRequestId">
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('mrdiff')"
+ :class="{
+ 'is-active': viewer === 'mrdiff',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('Compare changes with the merge request target branch') }}
+ </span>
+ </a>
+ </li>
+ <li
+ role="separator"
+ class="divider"
+ >
+ </li>
+ </template>
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 015e750525a..1c237c0ec97 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,49 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import ideSidebar from './ide_side_bar.vue';
- import ideContextbar from './ide_context_bar.vue';
- import repoTabs from './repo_tabs.vue';
- import repoFileButtons from './repo_file_buttons.vue';
- import ideStatusBar from './ide_status_bar.vue';
- import repoEditor from './repo_editor.vue';
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoEditor from './repo_editor.vue';
- export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
+export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ ideStatusBar,
+ repoEditor,
+ },
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
},
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer']),
- ...mapGetters(['activeFile', 'hasChanges']),
+ committedStateSvgPath: {
+ type: String,
+ required: true,
},
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ },
+ computed: {
+ ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...mapGetters(['activeFile', 'hasChanges']),
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
- };
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
</script>
<template>
@@ -60,17 +58,16 @@
v-if="activeFile"
>
<repo-tabs
+ :active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
- <repo-file-buttons
- :file="activeFile"
- />
<ide-status-bar
:file="activeFile"
/>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
new file mode 100644
index 00000000000..6d07329df71
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -0,0 +1,83 @@
+<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showButtons() {
+ return (
+ this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
+ );
+ },
+ rawDownloadButtonLabel() {
+ return this.file.binary ? __('Download') : __('Raw');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="showButtons"
+ class="pull-right ide-btn-group"
+ >
+ <a
+ v-tooltip
+ :href="file.blamePath"
+ :title="__('Blame')"
+ class="btn btn-xs btn-transparent blame"
+ >
+ <icon
+ name="blame"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.commitsPath"
+ :title="__('History')"
+ class="btn btn-xs btn-transparent history"
+ >
+ <icon
+ name="history"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.permalink"
+ :title="__('Permalink')"
+ class="btn btn-xs btn-transparent permalink"
+ >
+ <icon
+ name="link"
+ :size="16"
+ />
+ </a>
+ <a
+ v-tooltip
+ :href="file.rawPath"
+ target="_blank"
+ class="btn btn-xs btn-transparent prepend-left-10 raw"
+ rel="noopener noreferrer"
+ :title="rawDownloadButtonLabel">
+ <icon
+ name="download"
+ :size="16"
+ />
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 00000000000..8a440902dfc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+};
+</script>
+
+<template>
+ <icon
+ name="git-merge"
+ v-tooltip
+ title="__('Part of merge request changes')"
+ css-classes="ide-file-changed-icon"
+ :size="12"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e73d1ce839f..8a709b31ea0 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,11 +1,17 @@
<script>
/* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
+import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
+import IdeFileButtons from './ide_file_buttons.vue';
export default {
+ components: {
+ ContentViewer,
+ IdeFileButtons,
+ },
props: {
file: {
type: Object,
@@ -13,31 +19,40 @@ export default {
},
},
computed: {
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'viewer',
- 'delayViewerUpdated',
- ]),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
+ ...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
+ editTabCSS() {
+ return {
+ active: this.file.viewMode === 'edit',
+ };
+ },
+ previewTabCSS() {
+ return {
+ active: this.file.viewMode === 'preview',
+ };
+ },
},
watch: {
file(oldVal, newVal) {
- if (newVal.path !== this.file.path) {
+ // Compare key to allow for files opened in review mode to be cached differently
+ if (newVal.key !== this.file.key) {
this.initMonaco();
}
},
- leftPanelCollapsed() {
- this.editor.updateDimensions();
- },
rightPanelCollapsed() {
this.editor.updateDimensions();
},
viewer() {
this.createEditorInstance();
},
+ panelResizing() {
+ if (!this.panelResizing) {
+ this.editor.updateDimensions();
+ }
+ },
},
beforeDestroy() {
this.editor.dispose();
@@ -59,6 +74,7 @@ export default {
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
+ 'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
@@ -68,9 +84,14 @@ export default {
this.editor.clearEditor();
- this.getRawFileData(this.file)
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ })
.then(() => {
- const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
+ const viewerPromise = this.delayViewerUpdated
+ ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
+ : Promise.resolve();
return viewerPromise;
})
@@ -78,7 +99,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
- .catch((err) => {
+ .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
@@ -101,9 +122,13 @@ export default {
this.model = this.editor.createModel(this.file);
- this.editor.attachModel(this.model);
+ if (this.viewer === 'mrdiff') {
+ this.editor.attachMergeRequestModel(this.model);
+ } else {
+ this.editor.attachModel(this.model);
+ }
- this.model.onChange((model) => {
+ this.model.onChange(model => {
const { file } = model;
if (file.active) {
@@ -147,15 +172,47 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-if="shouldHideEditor"
- v-html="file.html"
- >
+ class="ide-mode-tabs clearfix"
+ v-if="!shouldHideEditor">
+ <ul class="nav-links pull-left">
+ <li :class="editTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
+ <template v-if="viewer === 'editor'">
+ {{ __('Edit') }}
+ </template>
+ <template v-else>
+ {{ __('Review') }}
+ </template>
+ </a>
+ </li>
+ <li
+ v-if="file.previewMode"
+ :class="previewTabCSS">
+ <a
+ href="javascript:void(0);"
+ role="button"
+ @click.prevent="setFileViewMode({ file, viewMode:'preview' })">
+ {{ file.previewMode.previewTitle }}
+ </a>
+ </li>
+ </ul>
+ <ide-file-buttons
+ :file="file"
+ />
</div>
<div
- v-show="!shouldHideEditor"
+ v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
>
</div>
+ <content-viewer
+ v-if="!shouldHideEditor && file.viewMode === 'preview'"
+ :content="file.content || file.raw"
+ :path="file.path"
+ :project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 297b9c2628f..3b5068d4910 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
+ mrFileIcon,
},
props: {
file: {
@@ -56,18 +58,11 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
- if (
- this.isTree &&
- this.$router.currentRoute.path === `/project${this.file.url}`
- ) {
+ if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
- const delayPromise = this.file.changed
- ? Promise.resolve()
- : this.updateDelayViewerUpdated(true);
-
- return delayPromise.then(() => {
+ return this.updateDelayViewerUpdated(true).then(() => {
router.push(`/project${this.file.url}`);
});
},
@@ -102,11 +97,15 @@ export default {
:file="file"
/>
</span>
- <changed-file-icon
- :file="file"
- v-if="file.changed || file.tempFile"
- class="prepend-top-5 pull-right"
- />
+ <span class="pull-right">
+ <mr-file-icon
+ v-if="file.mrChange"
+ />
+ <changed-file-icon
+ :file="file"
+ v-if="file.changed || file.tempFile"
+ />
+ </span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
deleted file mode 100644
index 4ea8cf7504b..00000000000
--- a/app/assets/javascripts/ide/components/repo_file_buttons.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-export default {
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- showButtons() {
- return this.file.rawPath ||
- this.file.blamePath ||
- this.file.commitsPath ||
- this.file.permalink;
- },
- rawDownloadButtonLabel() {
- return this.file.binary ? 'Download' : 'Raw';
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="multi-file-editor-btn-group"
- >
- <a
- :href="file.rawPath"
- target="_blank"
- class="btn btn-default btn-sm raw"
- rel="noopener noreferrer">
- {{ rawDownloadButtonLabel }}
- </a>
-
- <div
- class="btn-group"
- role="group"
- aria-label="File actions"
- >
- <a
- :href="file.blamePath"
- class="btn btn-default btn-sm blame"
- >
- Blame
- </a>
- <a
- :href="file.commitsPath"
- class="btn btn-default btn-sm history"
- >
- History
- </a>
- <a
- :href="file.permalink"
- class="btn btn-default btn-sm permalink"
- >
- Permalink
- </a>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d5..97589e116c5 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
- import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ },
+ computed: {
+ lockTooltip() {
+ return `Locked by ${this.file.file_lock.user.name}`;
},
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- lockTooltip() {
- return `Locked by ${this.file.file_lock.user.name}`;
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index c337bc813e6..304a73ed1ad 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,60 +1,64 @@
<script>
- import { mapActions } from 'vuex';
+import { mapActions } from 'vuex';
- import fileIcon from '~/vue_shared/components/file_icon.vue';
- import icon from '~/vue_shared/components/icon.vue';
- import fileStatusIcon from './repo_file_status_icon.vue';
- import changedFileIcon from './changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
- export default {
- components: {
- fileStatusIcon,
- fileIcon,
- icon,
- changedFileIcon,
+export default {
+ components: {
+ FileStatusIcon,
+ FileIcon,
+ Icon,
+ ChangedFileIcon,
+ },
+ props: {
+ tab: {
+ type: Object,
+ required: true,
},
- props: {
- tab: {
- type: Object,
- required: true,
- },
+ },
+ data() {
+ return {
+ tabMouseOver: false,
+ };
+ },
+ computed: {
+ closeLabel() {
+ if (this.tab.changed || this.tab.tempFile) {
+ return `${this.tab.name} changed`;
+ }
+ return `Close ${this.tab.name}`;
},
- data() {
- return {
- tabMouseOver: false,
- };
- },
- computed: {
- closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
- return `${this.tab.name} changed`;
- }
- return `Close ${this.tab.name}`;
- },
- showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
- },
+ showChangedIcon() {
+ return this.tab.changed ? !this.tabMouseOver : false;
},
+ },
+
+ methods: {
+ ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
+ clickFile(tab) {
+ this.updateDelayViewerUpdated(true);
- methods: {
- ...mapActions([
- 'closeFile',
- ]),
- clickFile(tab) {
+ if (tab.pending) {
+ this.openPendingTab(tab);
+ } else {
this.$router.push(`/project${tab.url}`);
- },
- mouseOverTab() {
- if (this.tab.changed) {
- this.tabMouseOver = true;
- }
- },
- mouseOutTab() {
- if (this.tab.changed) {
- this.tabMouseOver = false;
- }
- },
+ }
+ },
+ mouseOverTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = true;
+ }
+ },
+ mouseOutTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = false;
+ }
},
- };
+ },
+};
</script>
<template>
@@ -66,7 +70,7 @@
<button
type="button"
class="multi-file-tab-close"
- @click.stop.prevent="closeFile(tab.path)"
+ @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
>
<icon
@@ -82,7 +86,9 @@
<div
class="multi-file-tab"
- :class="{active : tab.active }"
+ :class="{
+ active: tab.active
+ }"
:title="tab.url"
>
<file-icon
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 8ea64ddf84a..7bd646ba9b0 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,62 @@
<script>
- import { mapActions } from 'vuex';
- import RepoTab from './repo_tab.vue';
- import EditorMode from './editor_mode_dropdown.vue';
+import { mapActions } from 'vuex';
+import RepoTab from './repo_tab.vue';
+import EditorMode from './editor_mode_dropdown.vue';
+import router from '../ide_router';
- export default {
- components: {
- RepoTab,
- EditorMode,
+export default {
+ components: {
+ RepoTab,
+ EditorMode,
+ },
+ props: {
+ activeFile: {
+ type: Object,
+ required: true,
},
- props: {
- files: {
- type: Array,
- required: true,
- },
- viewer: {
- type: String,
- required: true,
- },
- hasChanges: {
- type: Boolean,
- required: true,
- },
+ files: {
+ type: Array,
+ required: true,
},
- data() {
- return {
- showShadow: false,
- };
+ viewer: {
+ type: String,
+ required: true,
},
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow =
- this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ hasChanges: {
+ type: Boolean,
+ required: true,
+ },
+ mergeRequestId: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- ...mapActions(['updateViewer']),
+ },
+ data() {
+ return {
+ showShadow: false,
+ };
+ },
+ updated() {
+ if (!this.$refs.tabsScroller) return;
+
+ this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ },
+ methods: {
+ ...mapActions(['updateViewer', 'removePendingTab']),
+ openFileViewer(viewer) {
+ this.updateViewer(viewer);
+
+ if (this.activeFile.pending) {
+ return this.removePendingTab(this.activeFile).then(() => {
+ router.push(`/project${this.activeFile.url}`);
+ });
+ }
+
+ return null;
},
- };
+ },
+};
</script>
<template>
@@ -55,7 +75,8 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
- @click="updateViewer"
+ :merge-request-id="mergeRequestId"
+ @click="openFileViewer"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index faa690ecba0..5ea2a2f6825 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -1,67 +1,64 @@
<script>
- import { mapActions, mapState } from 'vuex';
- import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { mapActions, mapState } from 'vuex';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
- export default {
- components: {
- PanelResizer,
+export default {
+ components: {
+ PanelResizer,
+ },
+ props: {
+ collapsible: {
+ type: Boolean,
+ required: true,
},
- props: {
- collapsible: {
- type: Boolean,
- required: true,
- },
- initialWidth: {
- type: Number,
- required: true,
- },
- minSize: {
- type: Number,
- required: false,
- default: 200,
- },
- side: {
- type: String,
- required: true,
- },
+ initialWidth: {
+ type: Number,
+ required: true,
},
- data() {
- return {
- width: this.initialWidth,
- };
+ minSize: {
+ type: Number,
+ required: false,
+ default: 340,
},
- computed: {
- ...mapState({
- collapsed(state) {
- return state[`${this.side}PanelCollapsed`];
- },
- }),
- panelStyle() {
- if (!this.collapsed) {
- return {
- width: `${this.width}px`,
- };
- }
-
- return {};
- },
+ side: {
+ type: String,
+ required: true,
},
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleFullbarCollapsed() {
- if (this.collapsed && this.collapsible) {
- this.setPanelCollapsedStatus({
- side: this.side,
- collapsed: !this.collapsed,
- });
- }
+ },
+ data() {
+ return {
+ width: this.initialWidth,
+ };
+ },
+ computed: {
+ ...mapState({
+ collapsed(state) {
+ return state[`${this.side}PanelCollapsed`];
},
+ }),
+ panelStyle() {
+ if (!this.collapsed) {
+ return {
+ width: `${this.width}px`,
+ };
+ }
+
+ return {};
+ },
+ },
+ methods: {
+ ...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
+ toggleFullbarCollapsed() {
+ if (this.collapsed && this.collapsible) {
+ this.setPanelCollapsedStatus({
+ side: this.side,
+ collapsed: !this.collapsed,
+ });
+ }
},
- maxSize: (window.innerWidth / 2),
- };
+ },
+ maxSize: window.innerWidth / 2,
+};
</script>
<template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db..20983666b4a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
- path: 'mr/:mrid',
+ path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
@@ -76,10 +76,12 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
- to.params[0].slice(-1) === '/'
- ? to.params[0].slice(0, -1)
- : to.params[0];
- const treeEntry = store.state.entries[path];
+ to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
+ const treeEntryKey = Object.keys(store.state.entries).find(
+ key => key === path && !store.state.entries[key].pending,
+ );
+ const treeEntry = store.state.entries[treeEntryKey];
+
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
@@ -96,6 +98,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
+ } else if (to.params.mrid) {
+ store.dispatch('updateViewer', 'mrdiff');
+
+ store
+ .dispatch('getMergeRequestData', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ })
+ .then(mr => {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+
+ return store.dispatch('getFiles', {
+ projectId: fullProjectId,
+ branchId: mr.source_branch,
+ });
+ })
+ .then(() =>
+ store.dispatch('getMergeRequestVersions', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(() =>
+ store.dispatch('getMergeRequestChanges', {
+ projectId: fullProjectId,
+ mergeRequestId: to.params.mrid,
+ }),
+ )
+ .then(mrChanges => {
+ mrChanges.changes.forEach((change, ind) => {
+ const changeTreeEntry = store.state.entries[change.new_path];
+
+ if (changeTreeEntry) {
+ store.dispatch('setFileMrChange', {
+ file: changeTreeEntry,
+ mrChange: change,
+ });
+
+ if (ind < 10) {
+ store.dispatch('getFileData', {
+ path: change.new_path,
+ makeFileActive: ind === 0,
+ });
+ }
+ }
+ });
+ })
+ .catch(e => {
+ flash('Error while loading the merge request. Please try again.');
+ throw e;
+ });
}
})
.catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 73cd684351c..e47adae99ed 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,25 +13,31 @@ export default class Model {
(this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.file.path),
+ new this.monaco.Uri(null, null, this.file.key),
)),
);
+ if (this.file.mrChange) {
+ this.disposable.add(
+ (this.baseModel = this.monaco.editor.createModel(
+ this.file.baseRaw,
+ undefined,
+ new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ )),
+ );
+ }
this.events = new Map();
this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this);
- eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
- eventHub.$on(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
+ eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
@@ -47,7 +53,7 @@ export default class Model {
}
get path() {
- return this.file.path;
+ return this.file.key;
}
getModel() {
@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
+ getBaseModel() {
+ return this.baseModel;
+ }
+
setValue(value) {
this.getModel().setValue(value);
}
@@ -78,13 +88,7 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
- eventHub.$off(
- `editor.update.model.dispose.${this.file.path}`,
- this.dispose,
- );
- eventHub.$off(
- `editor.update.model.content.${this.file.path}`,
- this.updateContent,
- );
+ eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
+ eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 57d5e59a88b..0e7b563b5d6 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -9,17 +9,17 @@ export default class ModelManager {
this.models = new Map();
}
- hasCachedModel(path) {
- return this.models.has(path);
+ hasCachedModel(key) {
+ return this.models.has(key);
}
- getModel(path) {
- return this.models.get(path);
+ getModel(key) {
+ return this.models.get(key);
}
addModel(file) {
- if (this.hasCachedModel(file.path)) {
- return this.getModel(file.path);
+ if (this.hasCachedModel(file.key)) {
+ return this.getModel(file.key);
}
const model = new Model(this.monaco, file);
@@ -27,7 +27,7 @@ export default class ModelManager {
this.disposable.add(model);
eventHub.$on(
- `editor.update.model.dispose.${file.path}`,
+ `editor.update.model.dispose.${file.key}`,
this.removeCachedModel.bind(this, file),
);
@@ -35,12 +35,9 @@ export default class ModelManager {
}
removeCachedModel(file) {
- this.models.delete(file.path);
+ this.models.delete(file.key);
- eventHub.$off(
- `editor.update.model.dispose.${file.path}`,
- this.removeCachedModel,
- );
+ eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
}
dispose() {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 38de2fe2b27..001737d6ee8 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -65,6 +65,11 @@ export default class Editor {
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderLineHighlight: 'none',
+ hideCursorInOverviewRuler: true,
+ renderSideBySide: Editor.renderSideBySide(domElement),
})),
);
@@ -77,7 +82,7 @@ export default class Editor {
}
attachModel(model) {
- if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
+ if (this.isDiffEditorType) {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
@@ -105,11 +110,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
+ attachMergeRequestModel(model) {
+ this.instance.setModel({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+
+ this.monaco.editor.createDiffNavigator(this.instance, {
+ alwaysRevealFirst: true,
+ });
+ }
+
setupMonacoTheme() {
- this.monaco.editor.defineTheme(
- gitlabTheme.themeName,
- gitlabTheme.monacoTheme,
- );
+ this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
@@ -141,6 +154,7 @@ export default class Editor {
updateDimensions() {
this.instance.layout();
+ this.updateDiffView();
}
setPosition({ lineNumber, column }) {
@@ -157,8 +171,22 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
- this.disposable.add(
- this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
- );
+ this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
+ }
+
+ updateDiffView() {
+ if (!this.isDiffEditorType) return;
+
+ this.instance.updateOptions({
+ renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()),
+ });
+ }
+
+ get isDiffEditorType() {
+ return this.instance.getEditorType() === 'vs.editor.IDiffEditor';
+ }
+
+ static renderSideBySide(domElement) {
+ return domElement.offsetWidth >= 700;
}
}
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index d69d4b8c615..9f895d49f2e 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -6,6 +6,7 @@ export const defaultEditorOptions = {
minimap: {
enabled: false,
},
+ wordWrap: 'on',
};
export default [
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843..a12e637616a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ },
+ getBaseRawFileData(file, sha) {
+ if (file.tempFile) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ if (file.baseRaw) {
+ return Promise.resolve(file.baseRaw);
+ }
+
+ return Vue.http
+ .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+ params: { format: 'json' },
+ })
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
+ getProjectMergeRequestData(projectId, mergeRequestId) {
+ return Api.mergeRequest(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestChanges(projectId, mergeRequestId) {
+ return Api.mergeRequestChanges(projectId, mergeRequestId);
+ },
+ getProjectMergeRequestVersions(projectId, mergeRequestId) {
+ return Api.mergeRequestVersions(projectId, mergeRequestId);
+ },
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7e920aa9f30..c6ba679d99c 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
-export const setInitialData = ({ commit }, data) =>
- commit(types.SET_INITIAL_DATA, data);
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
@@ -22,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
};
export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', file.path));
+ state.openFiles.forEach(file => dispatch('closeFile', file));
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
@@ -43,14 +42,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
- const fullName =
- name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
- `The name "${name
- .split('/')
- .pop()}" is already taken in this directory.`,
+ `The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index ddc4b757bf9..1a17320a1ea 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
-export const closeFile = ({ commit, state, getters, dispatch }, path) => {
- const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
- const file = state.entries[path];
+export const closeFile = ({ commit, state, dispatch }, file) => {
+ const path = file.path;
+ const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active;
- commit(types.TOGGLE_FILE_OPEN, path);
- commit(types.SET_FILE_ACTIVE, { path, active: false });
+ if (file.pending) {
+ commit(types.REMOVE_PENDING_TAB, file);
+ } else {
+ commit(types.TOGGLE_FILE_OPEN, path);
+ commit(types.SET_FILE_ACTIVE, { path, active: false });
+ }
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
- const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
-
- router.push(`/project${nextFileToOpen.url}`);
+ const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+ if (nextFileToOpen.pending) {
+ dispatch('updateViewer', 'diff');
+ dispatch('openPendingTab', nextFileToOpen);
+ } else {
+ dispatch('updateDelayViewerUpdated', true);
+ router.push(`/project${nextFileToOpen.url}`);
+ }
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
- eventHub.$emit(`editor.update.model.dispose.${file.path}`);
+ eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
@@ -46,53 +56,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+ const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
-
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file.path);
- dispatch('setFileActive', file.path);
+ commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash(
- 'Error loading file data. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- );
+ flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
-export const getRawFileData = ({ commit, dispatch }, file) =>
- service
- .getRawFileData(file)
- .then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() =>
- flash(
- 'Error loading file content. Please try again.',
- 'alert',
- document,
- null,
- false,
- true,
- ),
- );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+ commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+ const file = state.entries[path];
+ return new Promise((resolve, reject) => {
+ service
+ .getRawFileData(file)
+ .then(raw => {
+ commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (file.mrChange && file.mrChange.new_file === false) {
+ service
+ .getBaseRawFileData(file, baseSha)
+ .then(baseRaw => {
+ commit(types.SET_FILE_BASE_RAW_DATA, {
+ file,
+ baseRaw,
+ });
+ resolve(raw);
+ })
+ .catch(e => {
+ reject(e);
+ });
+ } else {
+ resolve(raw);
+ }
+ })
+ .catch(() => {
+ flash('Error loading file content. Please try again.');
+ reject();
+ });
+ });
+};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
@@ -119,10 +139,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
-export const setEditorPosition = (
- { getters, commit },
- { editorRow, editorColumn },
-) => {
+export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
@@ -132,6 +149,10 @@ export const setEditorPosition = (
}
};
+export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
+ commit(types.SET_FILE_VIEWMODE, { file, viewMode });
+};
+
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
@@ -144,3 +165,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
};
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
+ if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+ return false;
+ }
+
+ commit(types.ADD_PENDING_TAB, { file });
+
+ dispatch('scrollToTab');
+
+ router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
+
+ return true;
+};
+
+export const removePendingTab = ({ commit }, file) => {
+ commit(types.REMOVE_PENDING_TAB, file);
+
+ eventHub.$emit(`editor.update.model.dispose.${file.key}`);
+};
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 00000000000..da73034fd7d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+ service
+ .getProjectMergeRequestData(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST, {
+ projectPath: projectId,
+ mergeRequestId,
+ mergeRequest: data,
+ });
+ if (!state.currentMergeRequestId) {
+ commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+ }
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request data. Please try again.');
+ reject(new Error(`Merge Request not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+ }
+ });
+
+export const getMergeRequestChanges = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+ service
+ .getProjectMergeRequestChanges(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_CHANGES, {
+ projectPath: projectId,
+ mergeRequestId,
+ changes: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request changes. Please try again.');
+ reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+ }
+ });
+
+export const getMergeRequestVersions = (
+ { commit, state, dispatch },
+ { projectId, mergeRequestId, force = false } = {},
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+ service
+ .getProjectMergeRequestVersions(projectId, mergeRequestId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_MERGE_REQUEST_VERSIONS, {
+ projectPath: projectId,
+ mergeRequestId,
+ versions: data,
+ });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading merge request versions. Please try again.');
+ reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+ });
+ } else {
+ resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a0325..6536be04f0a 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
-import {
- findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
- dispatch('getFileData', row);
+ dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
+ service
+ .getTreeLastCommit(tree.lastCommitPath)
+ .then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
- .then((data) => {
- data.forEach((lastCommit) => {
+ .then(data => {
+ data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
-export const getFiles = (
- { state, commit, dispatch },
- { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
- if (!state.trees[`${projectId}/${branchId}`]) {
- const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
- service
- .getFiles(selectedProject.web_url, branchId)
- .then(res => res.json())
- .then((data) => {
- const worker = new FilesDecoratorWorker();
- worker.addEventListener('message', (e) => {
- const { entries, treeList } = e.data;
- const selectedTree = state.trees[`${projectId}/${branchId}`];
-
- commit(types.SET_ENTRIES, entries);
- commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
- commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
- worker.terminate();
-
- resolve();
- });
-
- worker.postMessage({
- data,
- projectId,
- branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+ new Promise((resolve, reject) => {
+ if (!state.trees[`${projectId}/${branchId}`]) {
+ const selectedProject = state.projects[projectId];
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+ service
+ .getFiles(selectedProject.web_url, branchId)
+ .then(res => res.json())
+ .then(data => {
+ const worker = new FilesDecoratorWorker();
+ worker.addEventListener('message', e => {
+ const { entries, treeList } = e.data;
+ const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+ commit(types.SET_ENTRIES, entries);
+ commit(types.SET_DIRECTORY_DATA, {
+ treePath: `${projectId}/${branchId}`,
+ data: treeList,
+ });
+ commit(types.TOGGLE_LOADING, {
+ entry: selectedTree,
+ forceValue: false,
+ });
+
+ worker.terminate();
+
+ resolve();
+ });
+
+ worker.postMessage({
+ data,
+ projectId,
+ branchId,
+ });
+ })
+ .catch(e => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
});
- })
- .catch((e) => {
- flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
- reject(e);
- });
- } else {
- resolve();
- }
-});
-
+ } else {
+ resolve();
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df..a77cdbc13c8 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
- state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
-export const modifiedFiles = state =>
- state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
};
});
+export const currentMergeRequest = state => {
+ if (state.projects[state.currentProjectId]) {
+ return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+ }
+ return null;
+};
+
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e28f190897c..e3f504e5ab0 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,9 +34,11 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
@@ -39,5 +47,9 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
+export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+
+export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
+export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c..5e5eb831662 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
- loading:
- forceValue !== undefined
- ? forceValue
- : !state.entries[entry.path].loading,
+ loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(
- data.treeList,
- ),
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
@@ -100,6 +96,7 @@ export default {
});
},
...projectMutations,
+ ...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 2500f13db7c..6a143e518f9 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -5,6 +5,14 @@ export default {
Object.assign(state.entries[path], {
active,
});
+
+ if (active && !state.entries[path].pending) {
+ Object.assign(state, {
+ openFiles: state.openFiles.map(f =>
+ Object.assign(f, { active: f.pending ? false : f.active }),
+ ),
+ });
+ }
},
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], {
@@ -12,10 +20,14 @@ export default {
});
if (state.entries[path].opened) {
- state.openFiles.push(state.entries[path]);
+ Object.assign(state, {
+ openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
+ });
} else {
+ const file = state.entries[path];
+
Object.assign(state, {
- openFiles: state.openFiles.filter(f => f.path !== path),
+ openFiles: state.openFiles.filter(f => f.key !== file.key),
});
}
},
@@ -28,6 +40,9 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
+ raw: null,
+ baseRaw: null,
+ html: data.html,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -35,6 +50,11 @@ export default {
raw,
});
},
+ [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+ Object.assign(state.entries[file.path], {
+ baseRaw,
+ });
+ },
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
@@ -59,6 +79,16 @@ export default {
editorColumn,
});
},
+ [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+ Object.assign(state.entries[file.path], {
+ mrChange,
+ });
+ },
+ [types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
+ Object.assign(state.entries[file.path], {
+ viewMode,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
@@ -80,4 +110,37 @@ export default {
changed,
});
},
+ [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
+ const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
+ let openFiles = state.openFiles.map(f =>
+ Object.assign(f, { active: f.path === file.path, opened: false }),
+ );
+
+ if (!pendingTab) {
+ const openFile = openFiles.find(f => f.path === file.path);
+
+ openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
+ if (!f) return acc;
+
+ if (f.path === file.path) {
+ return acc.concat({
+ ...f,
+ active: true,
+ pending: true,
+ opened: true,
+ key: `${keyPrefix}-${f.key}`,
+ });
+ }
+
+ return acc.concat(f);
+ }, []);
+ }
+
+ Object.assign(state, { openFiles });
+ },
+ [types.REMOVE_PENDING_TAB](state, file) {
+ Object.assign(state, {
+ openFiles: state.openFiles.filter(f => f.key !== file.key),
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 00000000000..334819fe702
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+ Object.assign(state, {
+ currentMergeRequestId,
+ });
+ },
+ [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+ Object.assign(state.projects[projectPath], {
+ mergeRequests: {
+ [mergeRequestId]: {
+ ...mergeRequest,
+ active: true,
+ changes: [],
+ versions: [],
+ baseCommitSha: null,
+ },
+ },
+ });
+ },
+ [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ changes,
+ });
+ },
+ [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+ Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+ versions,
+ baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a919..284b39a2c72 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
+ mergeRequests: {},
active: true,
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c..e5cc8814000 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
currentProjectId: '',
currentBranchId: '',
+ currentMergeRequestId: '',
changedFiles: [],
endpoints: {},
lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e..4befcc501ef 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,5 +1,7 @@
export const dataStructure = () => ({
id: '',
+ // Key will contain a mixture of ID and path
+ // it can also contain a prefix `pending-` for files opened in review mode
key: '',
type: '',
projectId: '',
@@ -36,9 +38,11 @@ export const dataStructure = () => ({
editorColumn: 1,
fileLanguage: '',
eol: '',
+ viewMode: 'edit',
+ previewMode: null,
});
-export const decorateData = (entity) => {
+export const decorateData = entity => {
const {
id,
projectId,
@@ -55,9 +59,9 @@ export const decorateData = (entity) => {
changed = false,
parentTreeUrl = '',
base64 = false,
-
+ previewMode,
file_lock,
-
+ html,
} = entity;
return {
@@ -78,19 +82,18 @@ export const decorateData = (entity) => {
renderError,
content,
base64,
-
+ previewMode,
file_lock,
-
+ html,
};
};
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
- f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, prop = 'name') =>
+ tree.find(f => f.type === type && f[prop] === name);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
-export const setPageTitle = (title) => {
+export const setPageTitle = title => {
document.title = title;
};
@@ -120,6 +123,11 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
- tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+ sortedTree
+ .map(entity =>
+ Object.assign(entity, {
+ tree: entity.tree.length ? sortTree(entity.tree) : [],
+ }),
+ )
+ .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a4cd1ab099f..a1673276900 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -1,14 +1,8 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../utils';
self.addEventListener('message', e => {
- const {
- data,
- projectId,
- branchId,
- tempFile = false,
- content = '',
- base64 = false,
- } = e.data;
+ const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
@@ -19,9 +13,7 @@ self.addEventListener('message', e => {
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
- const folderPath = `${
- parentFolder ? `${parentFolder.path}/` : ''
- }${folderName}`;
+ const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
@@ -33,9 +25,7 @@ self.addEventListener('message', e => {
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
type: 'tree',
- parentTreeUrl: parentFolder
- ? parentFolder.url
- : `/${projectId}/tree/${branchId}/`,
+ parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
@@ -70,13 +60,12 @@ self.addEventListener('message', e => {
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
- parentTreeUrl: fileFolder
- ? fileFolder.url
- : `/${projectId}/blob/${branchId}`,
+ parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
+ previewMode: viewerInformationForPath(blobName),
});
Object.assign(acc, {
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index a6819aaeb12..dfe87d89a39 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -11,11 +11,19 @@
type: String,
required: true,
},
+ helpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
hasTitle() {
return this.title.length > 0;
},
+ hasHelpURL() {
+ return this.helpUrl.length > 0;
+ },
},
};
</script>
@@ -28,5 +36,21 @@
{{ title }}:
</span>
{{ value }}
+
+ <span
+ v-if="hasHelpURL"
+ class="help-button pull-right"
+ >
+ <a
+ :href="helpUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ >
+ <i
+ class="fa fa-question-circle"
+ aria-hidden="true"
+ ></i>
+ </a>
+ </span>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 56814a52525..af47056d98f 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -22,6 +22,11 @@
type: Boolean,
required: true,
},
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
shouldRenderContent() {
@@ -39,6 +44,21 @@
runnerId() {
return `#${this.job.runner.id}`;
},
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
+
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
+
+ return t;
+ },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
@@ -115,6 +135,13 @@
:value="queued"
/>
<detail-row
+ class="js-job-timeout"
+ v-if="hasTimeout"
+ title="Timeout"
+ :help-url="runnerHelpUrl"
+ :value="timeout"
+ />
+ <detail-row
class="js-job-runner"
v-if="job.runner"
title="Runner"
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 85a88ae409b..656676ead91 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -51,6 +51,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
+ runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 0830ebe9e4e..9ff2042475b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771f..dd17544b656 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
- params.forEach((param) => {
+ params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
+
+export function webIDEUrl(route = undefined) {
+ let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ if (route) {
+ returnUrl += `project${route}`;
+ }
+ return returnUrl;
+}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index add07c156a4..c749042a14a 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -94,10 +94,10 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
- <li data-milestone-id="${milestone.name}">
+ <li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
@@ -125,7 +125,6 @@ export default class MilestoneSelect {
return milestone.id;
}
},
- isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
@@ -137,7 +136,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
@@ -158,6 +157,7 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
+
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 10b3a4d2fee..f5572be5fbf 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,162 +1,155 @@
<script>
- import _ from 'underscore';
- import Flash from '../../flash';
- import MonitoringService from '../services/monitoring_service';
- import GraphGroup from './graph_group.vue';
- import Graph from './graph.vue';
- import EmptyState from './empty_state.vue';
- import MonitoringStore from '../stores/monitoring_store';
- import eventHub from '../event_hub';
+import _ from 'underscore';
+import Flash from '../../flash';
+import MonitoringService from '../services/monitoring_service';
+import GraphGroup from './graph_group.vue';
+import Graph from './graph.vue';
+import EmptyState from './empty_state.vue';
+import MonitoringStore from '../stores/monitoring_store';
+import eventHub from '../event_hub';
- export default {
- components: {
- Graph,
- GraphGroup,
- EmptyState,
+export default {
+ components: {
+ Graph,
+ GraphGroup,
+ EmptyState,
+ },
+ props: {
+ hasMetrics: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- props: {
- hasMetrics: {
- type: Boolean,
- required: false,
- default: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- showPanels: {
- type: Boolean,
- required: false,
- default: true,
- },
- forceSmallGraph: {
- type: Boolean,
- required: false,
- default: false,
- },
- documentationPath: {
- type: String,
- required: true,
- },
- settingsPath: {
- type: String,
- required: true,
- },
- clustersPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
- },
- metricsEndpoint: {
- type: String,
- required: true,
- },
- deploymentEndpoint: {
- type: String,
- required: false,
- default: null,
- },
- emptyGettingStartedSvgPath: {
- type: String,
- required: true,
- },
- emptyLoadingSvgPath: {
- type: String,
- required: true,
- },
- emptyNoDataSvgPath: {
- type: String,
- required: true,
- },
- emptyUnableToConnectSvgPath: {
- type: String,
- required: true,
- },
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- data() {
- return {
- store: new MonitoringStore(),
- state: 'gettingStarted',
- showEmptyState: true,
- updateAspectRatio: false,
- updatedAspectRatios: 0,
- hoverData: {},
- resizeThrottled: {},
- };
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
},
-
- created() {
- this.service = new MonitoringService({
- metricsEndpoint: this.metricsEndpoint,
- deploymentEndpoint: this.deploymentEndpoint,
- });
- eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
- eventHub.$on('hoverChanged', this.hoverChanged);
+ forceSmallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
},
-
- beforeDestroy() {
- eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
- eventHub.$off('hoverChanged', this.hoverChanged);
- window.removeEventListener('resize', this.resizeThrottled, false);
+ documentationPath: {
+ type: String,
+ required: true,
},
-
- mounted() {
- this.resizeThrottled = _.throttle(this.resize, 600);
- if (!this.hasMetrics) {
- this.state = 'gettingStarted';
- } else {
- this.getGraphsData();
- window.addEventListener('resize', this.resizeThrottled, false);
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ metricsEndpoint: {
+ type: String,
+ required: true,
+ },
+ deploymentEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyNoDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ store: new MonitoringStore(),
+ state: 'gettingStarted',
+ showEmptyState: true,
+ updateAspectRatio: false,
+ updatedAspectRatios: 0,
+ hoverData: {},
+ resizeThrottled: {},
+ };
+ },
+ created() {
+ this.service = new MonitoringService({
+ metricsEndpoint: this.metricsEndpoint,
+ deploymentEndpoint: this.deploymentEndpoint,
+ });
+ eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$on('hoverChanged', this.hoverChanged);
+ },
+ beforeDestroy() {
+ eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
+ eventHub.$off('hoverChanged', this.hoverChanged);
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+ mounted() {
+ this.resizeThrottled = _.throttle(this.resize, 600);
+ if (!this.hasMetrics) {
+ this.state = 'gettingStarted';
+ } else {
+ this.getGraphsData();
+ window.addEventListener('resize', this.resizeThrottled, false);
+ }
+ },
+ methods: {
+ getGraphsData() {
+ this.state = 'loading';
+ Promise.all([
+ this.service.getGraphsData().then(data => this.store.storeMetrics(data)),
+ this.service
+ .getDeploymentData()
+ .then(data => this.store.storeDeploymentData(data))
+ .catch(() => new Flash('Error getting deployment information.')),
+ ])
+ .then(() => {
+ if (this.store.groups.length < 1) {
+ this.state = 'noData';
+ return;
+ }
+ this.showEmptyState = false;
+ })
+ .catch(() => {
+ this.state = 'unableToConnect';
+ });
+ },
+ resize() {
+ this.updateAspectRatio = true;
+ },
+ toggleAspectRatio() {
+ this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ if (this.store.getMetricsCount() === this.updatedAspectRatios) {
+ this.updateAspectRatio = !this.updateAspectRatio;
+ this.updatedAspectRatios = 0;
}
},
-
- methods: {
- getGraphsData() {
- this.state = 'loading';
- Promise.all([
- this.service.getGraphsData()
- .then(data => this.store.storeMetrics(data)),
- this.service.getDeploymentData()
- .then(data => this.store.storeDeploymentData(data))
- .catch(() => new Flash('Error getting deployment information.')),
- ])
- .then(() => {
- if (this.store.groups.length < 1) {
- this.state = 'noData';
- return;
- }
- this.showEmptyState = false;
- })
- .catch(() => { this.state = 'unableToConnect'; });
- },
-
- resize() {
- this.updateAspectRatio = true;
- },
-
- toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
- if (this.store.getMetricsCount() === this.updatedAspectRatios) {
- this.updateAspectRatio = !this.updateAspectRatio;
- this.updatedAspectRatios = 0;
- }
- },
-
- hoverChanged(data) {
- this.hoverData = data;
- },
+ hoverChanged(data) {
+ this.hoverData = data;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index fbf451fce68..c77f451c2d3 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,91 +1,90 @@
<script>
- export default {
- props: {
- documentationPath: {
- type: String,
- required: true,
- },
- settingsPath: {
- type: String,
- required: false,
- default: '',
- },
- clustersPath: {
- type: String,
- required: false,
- default: '',
- },
- selectedState: {
- type: String,
- required: true,
- },
- emptyGettingStartedSvgPath: {
- type: String,
- required: true,
- },
- emptyLoadingSvgPath: {
- type: String,
- required: true,
- },
- emptyNoDataSvgPath: {
- type: String,
- required: true,
- },
- emptyUnableToConnectSvgPath: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ clustersPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ selectedState: {
+ type: String,
+ required: true,
},
- data() {
- return {
- states: {
- gettingStarted: {
- svgUrl: this.emptyGettingStartedSvgPath,
- title: 'Get started with performance monitoring',
- description: `Stay updated about the performance and health
+ emptyGettingStartedSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyLoadingSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyNoDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyUnableToConnectSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ states: {
+ gettingStarted: {
+ svgUrl: this.emptyGettingStartedSvgPath,
+ title: 'Get started with performance monitoring',
+ description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`,
- buttonText: 'Install Prometheus on clusters',
- buttonPath: this.clustersPath,
- secondaryButtonText: 'Configure existing Prometheus',
- secondaryButtonPath: this.settingsPath,
- },
- loading: {
- svgUrl: this.emptyLoadingSvgPath,
- title: 'Waiting for performance data',
- description: `Creating graphs uses the data from the Prometheus server.
+ buttonText: 'Install Prometheus on clusters',
+ buttonPath: this.clustersPath,
+ secondaryButtonText: 'Configure existing Prometheus',
+ secondaryButtonPath: this.settingsPath,
+ },
+ loading: {
+ svgUrl: this.emptyLoadingSvgPath,
+ title: 'Waiting for performance data',
+ description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`,
- buttonText: 'View documentation',
- buttonPath: this.documentationPath,
- },
- noData: {
- svgUrl: this.emptyNoDataSvgPath,
- title: 'No data found',
- description: `You are connected to the Prometheus server, but there is currently
+ buttonText: 'View documentation',
+ buttonPath: this.documentationPath,
+ },
+ noData: {
+ svgUrl: this.emptyNoDataSvgPath,
+ title: 'No data found',
+ description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
- buttonText: 'Configure Prometheus',
- buttonPath: this.settingsPath,
- },
- unableToConnect: {
- svgUrl: this.emptyUnableToConnectSvgPath,
- title: 'Unable to connect to Prometheus server',
- description: 'Ensure connectivity is available from the GitLab server to the ',
- buttonText: 'View documentation',
- buttonPath: this.documentationPath,
- },
+ buttonText: 'Configure Prometheus',
+ buttonPath: this.settingsPath,
+ },
+ unableToConnect: {
+ svgUrl: this.emptyUnableToConnectSvgPath,
+ title: 'Unable to connect to Prometheus server',
+ description: 'Ensure connectivity is available from the GitLab server to the ',
+ buttonText: 'View documentation',
+ buttonPath: this.documentationPath,
},
- };
- },
- computed: {
- currentState() {
- return this.states[this.selectedState];
- },
-
- showButtonDescription() {
- if (this.selectedState === 'unableToConnect') return true;
- return false;
},
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState];
+ },
+ showButtonDescription() {
+ if (this.selectedState === 'unableToConnect') return true;
+ return false;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 42615d2bb8e..04d546fafa0 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,236 +1,229 @@
<script>
- import { scaleLinear, scaleTime } from 'd3-scale';
- import { axisLeft, axisBottom } from 'd3-axis';
- import { max, extent } from 'd3-array';
- import { select } from 'd3-selection';
- import GraphLegend from './graph/legend.vue';
- import GraphFlag from './graph/flag.vue';
- import GraphDeployment from './graph/deployment.vue';
- import GraphPath from './graph/path.vue';
- import MonitoringMixin from '../mixins/monitoring_mixins';
- import eventHub from '../event_hub';
- import measurements from '../utils/measurements';
- import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
- import createTimeSeries from '../utils/multiple_time_series';
- import bp from '../../breakpoints';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { axisLeft, axisBottom } from 'd3-axis';
+import { max, extent } from 'd3-array';
+import { select } from 'd3-selection';
+import GraphLegend from './graph/legend.vue';
+import GraphFlag from './graph/flag.vue';
+import GraphDeployment from './graph/deployment.vue';
+import GraphPath from './graph/path.vue';
+import MonitoringMixin from '../mixins/monitoring_mixins';
+import eventHub from '../event_hub';
+import measurements from '../utils/measurements';
+import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
+import createTimeSeries from '../utils/multiple_time_series';
+import bp from '../../breakpoints';
- const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
+const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
- export default {
- components: {
- GraphLegend,
- GraphFlag,
- GraphDeployment,
- GraphPath,
+export default {
+ components: {
+ GraphLegend,
+ GraphFlag,
+ GraphDeployment,
+ GraphPath,
+ },
+ mixins: [MonitoringMixin],
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
},
-
- mixins: [MonitoringMixin],
-
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- hoverData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectPath: {
- type: String,
- required: true,
- },
- tagsPath: {
- type: String,
- required: true,
- },
- showLegend: {
- type: Boolean,
- required: false,
- default: true,
- },
- smallGraph: {
- type: Boolean,
- required: false,
- default: false,
+ updateAspectRatio: {
+ type: Boolean,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: true,
+ },
+ hoverData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
+ showLegend: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ smallGraph: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ baseGraphHeight: 450,
+ baseGraphWidth: 600,
+ graphHeight: 450,
+ graphWidth: 600,
+ graphHeightOffset: 120,
+ margin: {},
+ unitOfDisplay: '',
+ yAxisLabel: '',
+ legendTitle: '',
+ reducedDeploymentData: [],
+ measurements: measurements.large,
+ currentData: {
+ time: new Date(),
+ value: 0,
},
+ currentDataIndex: 0,
+ currentXCoordinate: 0,
+ currentFlagPosition: 0,
+ showFlag: false,
+ showFlagContent: false,
+ timeSeries: [],
+ realPixelRatio: 1,
+ };
+ },
+ computed: {
+ outerViewBox() {
+ return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
-
- data() {
+ innerViewBox() {
+ return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
+ },
+ axisTransform() {
+ return `translate(70, ${this.graphHeight - 100})`;
+ },
+ paddingBottomRootSvg() {
return {
- baseGraphHeight: 450,
- baseGraphWidth: 600,
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- margin: {},
- unitOfDisplay: '',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentDataIndex: 0,
- currentXCoordinate: 0,
- currentFlagPosition: 0,
- showFlag: false,
- showFlagContent: false,
- timeSeries: [],
- realPixelRatio: 1,
+ paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
};
},
-
- computed: {
- outerViewBox() {
- return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
- },
-
- innerViewBox() {
- return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
- },
-
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
-
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
- };
- },
-
- deploymentFlagData() {
- return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
- },
+ deploymentFlagData() {
+ return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
-
- watch: {
- updateAspectRatio() {
- if (this.updateAspectRatio) {
- this.graphHeight = 450;
- this.graphWidth = 600;
- this.measurements = measurements.large;
- this.draw();
- eventHub.$emit('toggleAspectRatio');
- }
- },
-
- hoverData() {
- this.positionFlag();
- },
+ },
+ watch: {
+ updateAspectRatio() {
+ if (this.updateAspectRatio) {
+ this.graphHeight = 450;
+ this.graphWidth = 600;
+ this.measurements = measurements.large;
+ this.draw();
+ eventHub.$emit('toggleAspectRatio');
+ }
},
-
- mounted() {
- this.draw();
+ hoverData() {
+ this.positionFlag();
},
+ },
+ mounted() {
+ this.draw();
+ },
+ methods: {
+ draw() {
+ const breakpointSize = bp.getBreakpointSize();
+ const query = this.graphData.queries[0];
+ this.margin = measurements.large.margin;
+ if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
+ this.graphHeight = 300;
+ this.margin = measurements.small.margin;
+ this.measurements = measurements.small;
+ }
+ this.unitOfDisplay = query.unit || '';
+ this.yAxisLabel = this.graphData.y_label || 'Values';
+ this.legendTitle = query.label || 'Average';
+ this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
+ this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
+ this.baseGraphHeight = this.graphHeight;
+ this.baseGraphWidth = this.graphWidth;
- methods: {
- draw() {
- const breakpointSize = bp.getBreakpointSize();
- const query = this.graphData.queries[0];
- this.margin = measurements.large.margin;
- if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
- this.unitOfDisplay = query.unit || '';
- this.yAxisLabel = this.graphData.y_label || 'Values';
- this.legendTitle = query.label || 'Average';
- this.graphWidth = this.$refs.baseSvg.clientWidth -
- this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- this.baseGraphHeight = this.graphHeight;
- this.baseGraphWidth = this.graphWidth;
-
- // pixel offsets inside the svg and outside are not 1:1
- this.realPixelRatio = (this.$refs.baseSvg.clientWidth / this.baseGraphWidth);
-
- this.renderAxesPaths();
- this.formatDeployments();
- },
-
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
- const firstTimeSeries = this.timeSeries[0];
- const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
- const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
- const d0 = firstTimeSeries.values[overlayIndex - 1];
- const d1 = firstTimeSeries.values[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
- const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
+ // pixel offsets inside the svg and outside are not 1:1
+ this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth;
- eventHub.$emit('hoverChanged', {
- hoveredDate,
- currentDeployXPos,
- });
- },
+ this.renderAxesPaths();
+ this.formatDeployments();
+ },
+ handleMouseOverGraph(e) {
+ let point = this.$refs.graphData.createSVGPoint();
+ point.x = e.clientX;
+ point.y = e.clientY;
+ point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
+ point.x = point.x += 7;
+ const firstTimeSeries = this.timeSeries[0];
+ const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
+ const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
+ const d0 = firstTimeSeries.values[overlayIndex - 1];
+ const d1 = firstTimeSeries.values[overlayIndex];
+ if (d0 === undefined || d1 === undefined) return;
+ const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
+ const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
+ const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
+ const currentDeployXPos = this.mouseOverDeployInfo(point.x);
- renderAxesPaths() {
- this.timeSeries = createTimeSeries(
- this.graphData.queries,
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset,
- );
+ eventHub.$emit('hoverChanged', {
+ hoveredDate,
+ currentDeployXPos,
+ });
+ },
+ renderAxesPaths() {
+ this.timeSeries = createTimeSeries(
+ this.graphData.queries,
+ this.graphWidth,
+ this.graphHeight,
+ this.graphHeightOffset,
+ );
- if (!this.showLegend) {
- this.baseGraphHeight -= 50;
- } else if (this.timeSeries.length > 3) {
- this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
- }
+ if (!this.showLegend) {
+ this.baseGraphHeight -= 50;
+ } else if (this.timeSeries.length > 3) {
+ this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
+ }
- const axisXScale = d3.scaleTime()
- .range([0, this.graphWidth - 70]);
- const axisYScale = d3.scaleLinear()
- .range([this.graphHeight - this.graphHeightOffset, 0]);
+ const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
+ const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
- const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
- axisXScale.domain(d3.extent(allValues, d => d.time));
- axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
+ const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
+ axisXScale.domain(d3.extent(allValues, d => d.time));
+ axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
- const xAxis = d3.axisBottom()
- .scale(axisXScale)
- .ticks(this.graphWidth / 120)
- .tickFormat(timeScaleFormat);
+ const xAxis = d3
+ .axisBottom()
+ .scale(axisXScale)
+ .ticks(this.graphWidth / 120)
+ .tickFormat(timeScaleFormat);
- const yAxis = d3.axisLeft()
- .scale(axisYScale)
- .ticks(measurements.yTicks);
+ const yAxis = d3
+ .axisLeft()
+ .scale(axisYScale)
+ .ticks(measurements.yTicks);
- d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
+ d3
+ .select(this.$refs.baseSvg)
+ .select('.x-axis')
+ .call(xAxis);
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this).select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
- },
+ const width = this.graphWidth;
+ d3
+ .select(this.$refs.baseSvg)
+ .select('.y-axis')
+ .call(yAxis)
+ .selectAll('.tick')
+ .each(function createTickLines(d, i) {
+ if (i > 0) {
+ d3
+ .select(this)
+ .select('line')
+ .attr('x2', width)
+ .attr('class', 'axis-tick');
+ } // Avoid adding the class to the first tick, to prevent coloring
+ }); // This will select all of the ticks once they're rendered
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index 98c25307b74..4012191ceb9 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -1,32 +1,30 @@
<script>
- export default {
- props: {
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
+export default {
+ props: {
+ deploymentData: {
+ type: Array,
+ required: true,
},
-
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
+ graphHeight: {
+ type: Number,
+ required: true,
},
-
- methods: {
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
- },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
},
- };
+ },
+ computed: {
+ calculatedHeight() {
+ return this.graphHeight - this.graphHeightOffset;
+ },
+ },
+ methods: {
+ transformDeploymentGroup(deployment) {
+ return `translate(${Math.floor(deployment.xPos) - 5}, 20)`;
+ },
+ },
+};
</script>
<template>
<g class="deploy-info">
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 07aa6a3e5de..906c7c51f52 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -1,127 +1,119 @@
<script>
- import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
- import { formatRelevantDigits } from '../../../lib/utils/number_utils';
- import icon from '../../../vue_shared/components/icon.vue';
+import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+import icon from '../../../vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
- },
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- deploymentFlagData: {
- type: Object,
- required: false,
- default: null,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- realPixelRatio: {
- type: Number,
- required: true,
- },
- showFlagContent: {
- type: Boolean,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- currentDataIndex: {
- type: Number,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ currentXCoordinate: {
+ type: Number,
+ required: true,
},
-
- computed: {
- formatTime() {
- return this.deploymentFlagData ?
- timeFormat(this.deploymentFlagData.time) :
- timeFormat(this.currentData.time);
- },
-
- formatDate() {
- return this.deploymentFlagData ?
- dateFormat(this.deploymentFlagData.time) :
- dateFormat(this.currentData.time);
- },
-
- cursorStyle() {
- const xCoordinate = this.deploymentFlagData ?
- this.deploymentFlagData.xPos :
- this.currentXCoordinate;
-
- const offsetTop = 20 * this.realPixelRatio;
- const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
- const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
-
- return {
- top: `${offsetTop}px`,
- left: `${offsetLeft}px`,
- height: `${height}px`,
- };
- },
-
- flagOrientation() {
- if (this.currentXCoordinate * this.realPixelRatio > 120) {
- return 'left';
- }
- return 'right';
- },
+ currentData: {
+ type: Object,
+ required: true,
},
+ deploymentFlagData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
+ },
+ realPixelRatio: {
+ type: Number,
+ required: true,
+ },
+ showFlagContent: {
+ type: Boolean,
+ required: true,
+ },
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ formatTime() {
+ return this.deploymentFlagData
+ ? timeFormat(this.deploymentFlagData.time)
+ : timeFormat(this.currentData.time);
+ },
+ formatDate() {
+ return this.deploymentFlagData
+ ? dateFormat(this.deploymentFlagData.time)
+ : dateFormat(this.currentData.time);
+ },
+ cursorStyle() {
+ const xCoordinate = this.deploymentFlagData
+ ? this.deploymentFlagData.xPos
+ : this.currentXCoordinate;
- methods: {
- seriesMetricValue(series) {
- const index = this.deploymentFlagData ?
- this.deploymentFlagData.seriesIndex :
- this.currentDataIndex;
- const value = series.values[index] &&
- series.values[index].value;
- if (isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
- },
-
- seriesMetricLabel(index, series) {
- if (this.timeSeries.length < 2) {
- return this.legendTitle;
- }
- if (series.metricTag) {
- return series.metricTag;
- }
- return `series ${index + 1}`;
- },
+ const offsetTop = 20 * this.realPixelRatio;
+ const offsetLeft = (70 + xCoordinate) * this.realPixelRatio;
+ const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio;
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
- },
+ return {
+ top: `${offsetTop}px`,
+ left: `${offsetLeft}px`,
+ height: `${height}px`,
+ };
+ },
+ flagOrientation() {
+ if (this.currentXCoordinate * this.realPixelRatio > 120) {
+ return 'left';
+ }
+ return 'right';
+ },
+ },
+ methods: {
+ seriesMetricValue(series) {
+ const index = this.deploymentFlagData
+ ? this.deploymentFlagData.seriesIndex
+ : this.currentDataIndex;
+ const value = series.values[index] && series.values[index].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)}${this.unitOfDisplay}`;
+ },
+ seriesMetricLabel(index, series) {
+ if (this.timeSeries.length < 2) {
+ return this.legendTitle;
+ }
+ if (series.metricTag) {
+ return series.metricTag;
+ }
+ return `series ${index + 1}`;
+ },
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 3149397b61f..a7a058a9203 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -1,127 +1,119 @@
<script>
- import { formatRelevantDigits } from '../../../lib/utils/number_utils';
-
- export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- timeSeries: {
- type: Array,
- required: true,
- },
- unitOfDisplay: {
- type: String,
- required: true,
- },
- currentDataIndex: {
- type: Number,
- required: true,
- },
- showLegendGroup: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- seriesXPosition: 0,
- metricUsageXPosition: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate = (((this.graphHeight - this.margin.top)
- + this.measurements.axisLabelLineOffset) / 2) || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate = (((this.graphHeight - this.margin.top)
- + this.measurements.axisLabelLineOffset) / 2)
- + (this.yLabelWidth / 2) || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- - this.margin.right) || 0;
- },
-
- yPosition() {
- return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
- },
+import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+export default {
+ props: {
+ graphWidth: {
+ type: Number,
+ required: true,
},
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.metricUsageXPosition = 0;
- this.seriesXPosition = 0;
- if (this.$refs.legendTitleSvg != null) {
- this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
- }
- if (this.$refs.seriesTitleSvg != null) {
- this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
- }
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
- methods: {
- translateLegendGroup(index) {
- return `translate(0, ${12 * (index)})`;
- },
-
- formatMetricUsage(series) {
- const value = series.values[this.currentDataIndex] &&
- series.values[this.currentDataIndex].value;
- if (isNaN(value)) {
- return '-';
- }
- return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
- },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ margin: {
+ type: Object,
+ required: true,
+ },
+ measurements: {
+ type: Object,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
+ yAxisLabel: {
+ type: String,
+ required: true,
+ },
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ showLegendGroup: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ yLabelWidth: 0,
+ yLabelHeight: 0,
+ seriesXPosition: 0,
+ metricUsageXPosition: 0,
+ };
+ },
+ computed: {
+ textTransform() {
+ const yCoordinate =
+ (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
- createSeriesString(index, series) {
- if (series.metricTag) {
- return `${series.metricTag} ${this.formatMetricUsage(series)}`;
- }
- return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
- },
+ return `translate(15, ${yCoordinate}) rotate(-90)`;
+ },
+ rectTransform() {
+ const yCoordinate =
+ (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
+ this.yLabelWidth / 2 || 0;
- strokeDashArray(type) {
- if (type === 'dashed') return '6, 3';
- if (type === 'dotted') return '3, 3';
- return null;
- },
+ return `translate(0, ${yCoordinate}) rotate(-90)`;
+ },
+ xPosition() {
+ return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
+ },
+ yPosition() {
+ return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const bbox = this.$refs.ylabel.getBBox();
+ this.metricUsageXPosition = 0;
+ this.seriesXPosition = 0;
+ if (this.$refs.legendTitleSvg != null) {
+ this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
+ }
+ if (this.$refs.seriesTitleSvg != null) {
+ this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
+ }
+ this.yLabelWidth = bbox.width + 10; // Added some padding
+ this.yLabelHeight = bbox.height + 5;
+ });
+ },
+ methods: {
+ translateLegendGroup(index) {
+ return `translate(0, ${12 * index})`;
+ },
+ formatMetricUsage(series) {
+ const value =
+ series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
+ if (isNaN(value)) {
+ return '-';
+ }
+ return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
+ },
+ createSeriesString(index, series) {
+ if (series.metricTag) {
+ return `${series.metricTag} ${this.formatMetricUsage(series)}`;
+ }
+ return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
+ },
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
},
- };
+ },
+};
</script>
<template>
<g class="axis-label-container">
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index c9721c4cb01..881560124a5 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -1,36 +1,36 @@
<script>
- export default {
- props: {
- generatedLinePath: {
- type: String,
- required: true,
- },
- generatedAreaPath: {
- type: String,
- required: true,
- },
- lineStyle: {
- type: String,
- required: false,
- default: '',
- },
- lineColor: {
- type: String,
- required: true,
- },
- areaColor: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ generatedLinePath: {
+ type: String,
+ required: true,
},
- computed: {
- strokeDashArray() {
- if (this.lineStyle === 'dashed') return '3, 1';
- if (this.lineStyle === 'dotted') return '1, 1';
- return null;
- },
+ generatedAreaPath: {
+ type: String,
+ required: true,
},
- };
+ lineStyle: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lineColor: {
+ type: String,
+ required: true,
+ },
+ areaColor: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ strokeDashArray() {
+ if (this.lineStyle === 'dashed') return '3, 1';
+ if (this.lineStyle === 'dotted') return '1, 1';
+ return null;
+ },
+ },
+};
</script>
<template>
<g>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index f71cf614552..a6dbe42a8f0 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -1,17 +1,17 @@
<script>
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
- showPanels: {
- type: Boolean,
- required: false,
- default: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
},
- };
+ showPanels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 09f0ea37103..ac70ddb3ff4 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1190,12 +1190,12 @@ export default class Notes {
addForm = false;
let lineTypeSelector = '';
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
@@ -1809,9 +1809,11 @@ export default class Notes {
}
}
+ $closeBtn.text($closeBtn.data('originalText'));
+
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- axios
+ return axios
.post(`${formAction}?html=true`, formData)
.then(res => {
const note = res.data;
@@ -1928,8 +1930,6 @@ export default class Notes {
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});
-
- return $closeBtn.text($closeBtn.data('originalText'));
}
/**
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 90dcafd75b7..648fa6ff804 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,6 +99,10 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
+ supportQuickActions() {
+ // Disable quick actions support for Epics
+ return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
+ },
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
@@ -355,7 +359,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
- data-supports-quick-actions="true"
+ :data-supports-quick-actions="supportQuickActions"
aria-label="Description"
v-model="note"
ref="textarea"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cf579c5d4dc..476b15aca4a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -258,9 +258,7 @@ Please check your network connection and try again.`;
:key="note.id"
/>
</ul>
- <div
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
+ <div class="discussion-reply-holder">
<template v-if="!isReplying && canReply">
<div
class="btn-group-justified discussion-with-resolve-btn"
@@ -292,10 +290,12 @@ Please check your network connection and try again.`;
</button>
</div>
<div
+ v-if="note.resolvable"
class="btn-group discussion-actions"
- role="group">
+ role="group"
+ >
<div
- v-if="note.resolvable && !discussionResolved"
+ v-if="!discussionResolved"
class="btn-group"
role="group">
<a
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index a90c6d6381d..5bd81c7cad6 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,7 +50,11 @@ export default {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
- const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
+
+ if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
+ return EPIC_NOTEABLE_TYPE;
+ }
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index f4f407ffd8a..68f8cb1cf1e 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -10,6 +10,7 @@ export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
+export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index f90775d0157..e4121f151db 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -12,8 +12,11 @@ document.addEventListener(
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
let currentUserData = {};
+ noteableData.noteableType = notesDataset.noteableType;
+
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
@@ -25,7 +28,7 @@ document.addEventListener(
}
return {
- noteableData: JSON.parse(notesDataset.noteableData),
+ noteableData,
currentUserData,
notesData: JSON.parse(notesDataset.notesData),
};
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index 0da4ff49f08..5bf8216a1f3 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -14,6 +14,8 @@ export default {
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
+ case 'Epic':
+ return constants.EPIC_NOTEABLE_TYPE;
default:
return '';
}
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
new file mode 100644
index 00000000000..48d75f5443b
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -0,0 +1,6 @@
+import initSettingsPanels from '~/settings_panels';
+
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index d149b307e7f..914f804fdd3 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
+ isGroupDecendent: true,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index a5cc1f34b63..1600faa3611 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
+ isGroupDecendent: true,
});
projectSelect();
});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 22248418c41..2bda2aeb3a1 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -19,15 +19,19 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
- return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
+ return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged.
- This action cannot be reversed.`);
+ This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
},
},
methods: {
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index d00f81c9094..8e79341e96a 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -25,6 +25,7 @@ export default () => {
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
@@ -54,6 +55,7 @@ export default () => {
return {
modalProps: {
milestoneTitle: '',
+ groupName: '',
url: '',
},
};
diff --git a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
index 9ab73be80a0..9ab73be80a0 100644
--- a/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js
diff --git a/app/assets/javascripts/pages/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
index 8e8a843da0b..8e8a843da0b 100644
--- a/app/assets/javascripts/pages/ci/lints/new/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js
diff --git a/app/assets/javascripts/pages/ci/lints/show/index.js b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
index 8e8a843da0b..8e8a843da0b 100644
--- a/app/assets/javascripts/pages/ci/lints/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 54695dfeb99..ad6df51bb7a 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,4 +1,5 @@
<script>
+ import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
@@ -27,19 +28,26 @@
type: String,
required: true,
},
+ groupName: {
+ type: String,
+ required: true,
+ },
},
computed: {
text() {
- return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
- Existing project labels with the same title will be merged. This action cannot be reversed.`);
+ return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}.
+ Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+ labelTitle: this.labelTitle,
+ groupName: this.groupName,
+ });
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
- >${this.labelTitle}</span>`;
+ >${_.escape(this.labelTitle)}</span>`;
- return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
+ return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
labelTitle: label,
}, false);
},
@@ -69,6 +77,7 @@
>
<div
slot="title"
+ class="modal-title-with-label"
v-html="title"
>
{{ title }}
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 2abcbfab1ed..03cfef61311 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -30,6 +30,7 @@ const initLabelIndex = () => {
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
+ groupName: button.dataset.groupName,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
@@ -62,6 +63,7 @@ const initLabelIndex = () => {
labelColor: '',
labelTextColor: '',
url: '',
+ groupName: '',
},
};
},
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8a86c409b62..ceb02309959 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,59 +1,73 @@
<script>
- import Flash from '../../../flash';
- import editForm from './edit_form.vue';
- import Icon from '../../../vue_shared/components/icon.vue';
- import { __ } from '../../../locale';
+import Flash from '../../../flash';
+import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- isEditable: {
- required: true,
- type: Boolean,
- },
- service: {
- required: true,
- type: Object,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
},
- data() {
- return {
- edit: false,
- };
+ service: {
+ required: true,
+ type: Object,
},
- computed: {
- confidentialityIcon() {
- return this.isConfidential ? 'eye-slash' : 'eye';
- },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
- methods: {
- toggleForm() {
- this.edit = !this.edit;
- },
- updateConfidentialAttribute(confidential) {
- this.service.update('issue', { confidential })
- .then(() => location.reload())
- .catch(() => {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- });
- },
+ },
+ created() {
+ eventHub.$on('closeConfidentialityForm', this.toggleForm);
+ },
+ beforeDestroy() {
+ eventHub.$off('closeConfidentialityForm', this.toggleForm);
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
},
- };
+ updateConfidentialAttribute(confidential) {
+ this.service
+ .update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => {
+ Flash(
+ __(
+ 'Something went wrong trying to change the confidentiality of this issue',
+ ),
+ );
+ });
+ },
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="confidentialityIcon"
- :size="16"
aria-hidden="true"
/>
</div>
@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
- :toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index c569843b05f..3783f71a848 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,34 +1,34 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import { s__ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import { s__ } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
},
- props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
- updateConfidentialAttribute: {
- required: true,
- type: Function,
- },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
},
- computed: {
- confidentialityOnWarning() {
- return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
- },
- confidentialityOffWarning() {
- return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
- },
+ },
+ computed: {
+ confidentialityOnWarning() {
+ return s__(
+ 'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+ );
},
- };
+ confidentialityOffWarning() {
+ return s__(
+ 'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+ );
+ },
+ },
+};
</script>
<template>
@@ -45,7 +45,6 @@
</p>
<edit-form-buttons
:is-confidential="isConfidential"
- :toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 49d5dfeea1a..38b1ddbfd5b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,14 +1,13 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
updateConfidentialAttribute: {
required: true,
type: Function,
@@ -22,6 +21,16 @@ export default {
return !this.isConfidential;
},
},
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeConfidentialityForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateConfidentialAttribute(this.updateConfidentialBool);
+ },
+ },
};
</script>
@@ -30,14 +39,14 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
- @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ @click.prevent="submitForm"
>
{{ toggleButtonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index bc32e974bc3..e1e4715826a 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,43 @@
<script>
- import editFormButtons from './edit_form_buttons.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import { __, sprintf } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import { __, sprintf } from '../../../locale';
- export default {
- components: {
- editFormButtons,
+export default {
+ components: {
+ editFormButtons,
+ },
+ mixins: [issuableMixin],
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
},
- mixins: [
- issuableMixin,
- ],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
- toggleForm: {
- required: true,
- type: Function,
- },
-
- updateLockedAttribute: {
- required: true,
- type: Function,
- },
+ updateLockedAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ lockWarning() {
+ return sprintf(
+ __(
+ 'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- computed: {
- lockWarning() {
- return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
- unlockWarning() {
- return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
- },
+ unlockWarning() {
+ return sprintf(
+ __(
+ 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
+ ),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
},
- };
+ },
+};
</script>
<template>
@@ -54,7 +57,6 @@
<edit-form-buttons
:is-locked="isLocked"
- :toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3a553a7605..5e7b8f9698f 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,7 @@
<script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
export default {
props: {
isLocked: {
@@ -6,11 +9,6 @@ export default {
type: Boolean,
},
- toggleForm: {
- required: true,
- type: Function,
- },
-
updateLockedAttribute: {
required: true,
type: Function,
@@ -26,6 +24,17 @@ export default {
return !this.isLocked;
},
},
+
+ methods: {
+ closeForm() {
+ eventHub.$emit('closeLockForm');
+ $(this.$el).trigger('hidden.gl.dropdown');
+ },
+ submitForm() {
+ this.closeForm();
+ this.updateLockedAttribute(this.toggleLock);
+ },
+ },
};
</script>
@@ -34,7 +43,7 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
- @click="toggleForm"
+ @click="closeForm"
>
{{ __('Cancel') }}
</button>
@@ -42,7 +51,7 @@ export default {
<button
type="button"
class="btn btn-close"
- @click.prevent="updateLockedAttribute(toggleLock)"
+ @click.prevent="submitForm"
>
{{ buttonText }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 0686910fc7e..e4893451af3 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,70 +1,93 @@
<script>
- import Flash from '~/flash';
- import editForm from './edit_form.vue';
- import issuableMixin from '../../../vue_shared/mixins/issuable';
- import Icon from '../../../vue_shared/components/icon.vue';
+import Flash from '~/flash';
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
+import eventHub from '../../event_hub';
- export default {
- components: {
- editForm,
- Icon,
- },
- mixins: [
- issuableMixin,
- ],
+export default {
+ components: {
+ editForm,
+ Icon,
+ },
+ mixins: [issuableMixin],
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
+ props: {
+ isLocked: {
+ required: true,
+ type: Boolean,
+ },
- isEditable: {
- required: true,
- type: Boolean,
- },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
- mediator: {
- required: true,
- type: Object,
- validator(mediatorObject) {
- return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
- },
+ mediator: {
+ required: true,
+ type: Object,
+ validator(mediatorObject) {
+ return (
+ mediatorObject.service &&
+ mediatorObject.service.update &&
+ mediatorObject.store
+ );
},
},
+ },
- computed: {
- lockIcon() {
- return this.isLocked ? 'lock' : 'lock-open';
- },
+ computed: {
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
+ },
- isLockDialogOpen() {
- return this.mediator.store.isLockDialogOpen;
- },
+ isLockDialogOpen() {
+ return this.mediator.store.isLockDialogOpen;
},
+ },
- methods: {
- toggleForm() {
- this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
- },
+ created() {
+ eventHub.$on('closeLockForm', this.toggleForm);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('closeLockForm', this.toggleForm);
+ },
- updateLockedAttribute(locked) {
- this.mediator.service.update(this.issuableType, {
+ methods: {
+ toggleForm() {
+ this.mediator.store.isLockDialogOpen = !this.mediator.store
+ .isLockDialogOpen;
+ },
+
+ updateLockedAttribute(locked) {
+ this.mediator.service
+ .update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
- .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
- },
+ .catch(() =>
+ Flash(
+ this.__(
+ `Something went wrong trying to change the locked state of this ${
+ this.issuableDisplayName
+ }`,
+ ),
+ ),
+ );
},
- };
+ },
+};
</script>
<template>
<div class="block issuable-sidebar-item lock">
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="toggleForm"
+ >
<icon
:name="lockIcon"
- :size="16"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
- :toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index b5ebccd3795..82c4562f9a9 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,7 +1,8 @@
+<script>
import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default {
- name: 'time-tracking-comparison-pane',
+ name: 'TimeTrackingComparisonPane',
props: {
timeSpent: {
type: Number,
@@ -43,47 +44,50 @@ export default {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
},
- template: `
- <div class="time-tracking-comparison-pane">
+};
+</script>
+
+<template>
+ <div class="time-tracking-comparison-pane">
+ <div
+ class="compare-meter"
+ data-toggle="tooltip"
+ data-placement="top"
+ role="timeRemainingDisplay"
+ :aria-valuenow="timeRemainingTooltip"
+ :title="timeRemainingTooltip"
+ :data-original-title="timeRemainingTooltip"
+ :class="timeRemainingStatusClass"
+ >
<div
- class="compare-meter"
- data-toggle="tooltip"
- data-placement="top"
- role="timeRemainingDisplay"
- :aria-valuenow="timeRemainingTooltip"
- :title="timeRemainingTooltip"
- :data-original-title="timeRemainingTooltip"
- :class="timeRemainingStatusClass"
+ class="meter-container"
+ role="timeSpentPercent"
+ :aria-valuenow="timeRemainingPercent"
>
<div
- class="meter-container"
- role="timeSpentPercent"
- :aria-valuenow="timeRemainingPercent"
+ :style="{ width: timeRemainingPercent }"
+ class="meter-fill"
>
- <div
- :style="{ width: timeRemainingPercent }"
- class="meter-fill"
- />
</div>
- <div class="compare-display-container">
- <div class="compare-display pull-left">
- <span class="compare-label">
+ </div>
+ <div class="compare-display-container">
+ <div class="compare-display pull-left">
+ <span class="compare-label">
{{ s__('TimeTracking|Spent') }}
- </span>
- <span class="compare-value spent">
- {{ timeSpentHumanReadable }}
- </span>
- </div>
- <div class="compare-display estimated pull-right">
- <span class="compare-label">
- {{ s__('TimeTrackingEstimated|Est') }}
- </span>
- <span class="compare-value">
- {{ timeEstimateHumanReadable }}
- </span>
- </div>
+ </span>
+ <span class="compare-value spent">
+ {{ timeSpentHumanReadable }}
+ </span>
+ </div>
+ <div class="compare-display estimated pull-right">
+ <span class="compare-label">
+ {{ s__('TimeTrackingEstimated|Est') }}
+ </span>
+ <span class="compare-value">
+ {{ timeEstimateHumanReadable }}
+ </span>
</div>
</div>
</div>
- `,
-};
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 28240468d2c..1c641c73ea3 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -4,7 +4,7 @@ import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane';
import timeTrackingEstimateOnlyPane from './estimate_only_pane';
-import timeTrackingComparisonPane from './comparison_pane';
+import TimeTrackingComparisonPane from './comparison_pane.vue';
import eventHub from '../../event_hub';
@@ -15,7 +15,7 @@ export default {
'time-tracking-estimate-only-pane': timeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
- 'time-tracking-comparison-pane': timeTrackingComparisonPane,
+ TimeTrackingComparisonPane,
'time-tracking-help-state': timeTrackingHelpState,
},
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628..18ee4c62bf1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import { n__ } from '~/locale';
- import icon from '~/vue_shared/components/icon.vue';
- import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
- export default {
- name: 'MRWidgetHeader',
- directives: {
- tooltip,
+export default {
+ name: 'MRWidgetHeader',
+ directives: {
+ tooltip,
+ },
+ components: {
+ icon,
+ clipboardButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
},
- components: {
- icon,
- clipboardButton,
+ },
+ computed: {
+ shouldShowCommitsBehindText() {
+ return this.mr.divergedCommitsCount > 0;
},
- props: {
- mr: {
- type: Object,
- required: true,
- },
+ commitsText() {
+ return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
- computed: {
- shouldShowCommitsBehindText() {
- return this.mr.divergedCommitsCount > 0;
- },
- commitsText() {
- return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
- },
- branchNameClipboardData() {
- // This supports code in app/assets/javascripts/copy_to_clipboard.js that
- // works around ClipboardJS limitations to allow the context-specific
- // copy/pasting of plain text or GFM.
- return JSON.stringify({
- text: this.mr.sourceBranch,
- gfm: `\`${this.mr.sourceBranch}\``,
- });
- },
- isSourceBranchLong() {
- return this.isBranchTitleLong(this.mr.sourceBranch);
- },
- isTargetBranchLong() {
- return this.isBranchTitleLong(this.mr.targetBranch);
- },
+ branchNameClipboardData() {
+ // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+ // works around ClipboardJS limitations to allow the context-specific
+ // copy/pasting of plain text or GFM.
+ return JSON.stringify({
+ text: this.mr.sourceBranch,
+ gfm: `\`${this.mr.sourceBranch}\``,
+ });
},
- methods: {
- isBranchTitleLong(branchTitle) {
- return branchTitle.length > 32;
- },
+ isSourceBranchLong() {
+ return this.isBranchTitleLong(this.mr.sourceBranch);
},
- };
+ isTargetBranchLong() {
+ return this.isBranchTitleLong(this.mr.targetBranch);
+ },
+ webIdePath() {
+ return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+ },
+ },
+ methods: {
+ isBranchTitleLong(branchTitle) {
+ return branchTitle.length > 32;
+ },
+ },
+};
</script>
<template>
<div class="mr-source-target">
@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-sm btn-default inline js-web-ide"
+ >
+ {{ s__("mrWidget|Web IDE") }}
+ </a>
<button
data-target="#modal_merge_info"
data-toggle="modal"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 04100871a94..7cc07401911 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -17,8 +17,8 @@ export default {
/>
<div class="media-body space-children">
<span class="bold">
- The source branch HEAD has recently changed.
- Please reload the page and review the changes before merging.
+ {{ s__(`mrWidget|The source branch HEAD has recently changed.
+Please reload the page and review the changes before merging`) }}
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
new file mode 100644
index 00000000000..fb8ccea91c7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -0,0 +1,43 @@
+<script>
+import { viewerInformationForPath } from './lib/viewer_utils';
+import MarkdownViewer from './viewers/markdown_viewer.vue';
+
+export default {
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ viewer() {
+ const previewInfo = viewerInformationForPath(this.path);
+ switch (previewInfo.id) {
+ case 'markdown':
+ return MarkdownViewer;
+ default:
+ return null;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="preview-container">
+ <component
+ :is="viewer"
+ :project-path="projectPath"
+ :content="content"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
new file mode 100644
index 00000000000..4f2e1e47dd1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -0,0 +1,23 @@
+const viewers = {
+ markdown: {
+ id: 'markdown',
+ previewTitle: 'Preview Markdown',
+ },
+};
+
+const fileNameViewers = {};
+const fileExtensionViewers = {
+ md: 'markdown',
+ markdown: 'markdown',
+};
+
+export function viewerInformationForPath(path) {
+ if (!path) return null;
+ const name = path.split('/').pop();
+ const viewerName =
+ fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || '';
+
+ return viewers[viewerName];
+}
+
+export default viewers;
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
new file mode 100644
index 00000000000..09e0094054d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -0,0 +1,90 @@
+<script>
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import $ from 'jquery';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+
+const CancelToken = axios.CancelToken;
+let axiosSource;
+
+export default {
+ components: {
+ SkeletonLoadingContainer,
+ },
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ previewContent: null,
+ isLoading: false,
+ };
+ },
+ watch: {
+ content() {
+ this.previewContent = null;
+ },
+ },
+ created() {
+ axiosSource = CancelToken.source();
+ this.fetchMarkdownPreview();
+ },
+ updated() {
+ this.fetchMarkdownPreview();
+ },
+ destroyed() {
+ if (this.isLoading) axiosSource.cancel('Cancelling Preview');
+ },
+ methods: {
+ fetchMarkdownPreview() {
+ if (this.content && this.previewContent === null) {
+ this.isLoading = true;
+ const postBody = {
+ text: this.content,
+ };
+ const postOptions = {
+ cancelToken: axiosSource.token,
+ };
+
+ axios
+ .post(
+ `${gon.relative_url_root}/${this.projectPath}/preview_markdown`,
+ postBody,
+ postOptions,
+ )
+ .then(({ data }) => {
+ this.previewContent = data.body;
+ this.isLoading = false;
+
+ this.$nextTick(() => {
+ $(this.$refs['markdown-preview']).renderGFM();
+ });
+ })
+ .catch(() => {
+ this.previewContent = __('An error occurred while fetching markdown preview');
+ this.isLoading = false;
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="markdown-preview"
+ class="md md-previewer">
+ <skeleton-loading-container v-if="isLoading" />
+ <div
+ v-else
+ v-html="previewContent">
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index db36e27fa74..05cb0196ced 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -2,7 +2,15 @@
* Styles the GitLab application with a specific color theme
*/
-@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
+@mixin gitlab-theme(
+ $color-100,
+ $color-200,
+ $color-500,
+ $color-700,
+ $color-800,
+ $color-900,
+ $color-alternate
+) {
// Header
.navbar-gitlab {
@@ -23,7 +31,7 @@
> li {
> a:hover,
> a:focus {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
&.active > a,
@@ -33,7 +41,7 @@
}
&.line-separator {
- border-left: 1px solid rgba($color-200, .2);
+ border-left: 1px solid rgba($color-200, 0.2);
}
}
}
@@ -56,7 +64,7 @@
&:hover,
&:focus {
@media (min-width: $screen-sm-min) {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
svg {
@@ -91,34 +99,34 @@
> a {
&:hover,
&:focus {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
}
}
}
.search {
form {
- background-color: rgba($color-200, .2);
+ background-color: rgba($color-200, 0.2);
&:hover {
- background-color: rgba($color-200, .3);
+ background-color: rgba($color-200, 0.3);
}
}
.location-badge {
color: $color-100;
- background-color: rgba($color-200, .1);
+ background-color: rgba($color-200, 0.1);
border-right: 1px solid $color-800;
}
.search-input::placeholder {
- color: rgba($color-200, .8);
+ color: rgba($color-200, 0.8);
}
.search-input-wrap {
.search-icon,
.clear-icon {
- fill: rgba($color-200, .8);
+ fill: rgba($color-200, 0.8);
}
}
@@ -133,7 +141,7 @@
.search-input-wrap {
.search-icon {
- fill: rgba($color-200, .8);
+ fill: rgba($color-200, 0.8);
}
}
}
@@ -144,7 +152,6 @@
color: $color-900;
}
-
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $color-700;
@@ -169,28 +176,94 @@
font-weight: $gl-font-weight-bold;
}
}
-}
+ // Web IDE
+ .ide-sidebar-link {
+ color: $color-200;
+ background-color: $color-700;
+
+ &:hover,
+ &:focus {
+ background-color: $color-500;
+ }
+
+ &:active {
+ background: $color-800;
+ }
+ }
+
+ .branch-container {
+ border-left-color: $color-700;
+ }
+
+ .branch-header-title {
+ color: $color-700;
+ }
+
+ .ide-file-list .file.file-active {
+ color: $color-700;
+ }
+}
body {
&.ui_indigo {
- @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
+ @include gitlab-theme(
+ $indigo-100,
+ $indigo-200,
+ $indigo-500,
+ $indigo-700,
+ $indigo-800,
+ $indigo-900,
+ $white-light
+ );
}
&.ui_dark {
- @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
+ @include gitlab-theme(
+ $theme-gray-100,
+ $theme-gray-200,
+ $theme-gray-500,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-900,
+ $white-light
+ );
}
&.ui_blue {
- @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
+ @include gitlab-theme(
+ $theme-blue-100,
+ $theme-blue-200,
+ $theme-blue-500,
+ $theme-blue-700,
+ $theme-blue-800,
+ $theme-blue-900,
+ $white-light
+ );
}
&.ui_green {
- @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
+ @include gitlab-theme(
+ $theme-green-100,
+ $theme-green-200,
+ $theme-green-500,
+ $theme-green-700,
+ $theme-green-800,
+ $theme-green-900,
+ $white-light
+ );
}
&.ui_light {
- @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
+ @include gitlab-theme(
+ $theme-gray-900,
+ $theme-gray-700,
+ $theme-gray-800,
+ $theme-gray-700,
+ $theme-gray-700,
+ $theme-gray-100,
+ $theme-gray-700
+ );
.navbar-gitlab {
background-color: $theme-gray-100;
@@ -270,5 +343,9 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
+
+ .ide-sidebar-link {
+ color: $white-light;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 48b981dd31f..eb789cc64b0 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -4,9 +4,15 @@
.page-title,
.modal-title {
+ .modal-title-with-label span {
+ vertical-align: middle;
+ display: inline-block;
+ }
+
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
+ vertical-align: middle;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3dd4a613789..798f248dad4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -88,7 +88,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$header-height});
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7f037582ca0..679f783b1b6 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -813,6 +813,7 @@
}
.discussion-notes {
+ padding: 0 $gl-padding $gl-padding;
min-height: 35px;
&:first-child {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e21a9f0afc9..2c0ed976301 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -522,10 +522,6 @@
.with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height;
-
- .issuable-sidebar {
- height: calc(100% - #{$performance-bar-height});
- }
}
.sidebar-move-issue-confirmation-button {
diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss
deleted file mode 100644
index 68b6c5ecbd4..00000000000
--- a/app/assets/stylesheets/pages/lint.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-.ci-body {
- .incorrect-syntax {
- font-size: 18px;
- color: $lint-incorrect-color;
- }
-
- .correct-syntax {
- font-size: 18px;
- color: $lint-correct-color;
- }
-}
-
-.ci-linter {
- .ci-editor {
- height: 400px;
- }
-
- .ci-template pre {
- white-space: pre-wrap;
- }
-}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 4a528bc2bb1..8720f821ce9 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -173,11 +173,7 @@
}
.discussion-form {
- background-color: $white-light;
-}
-
-.discussion-form-container {
- padding: $gl-padding-top $gl-padding $gl-padding;
+ padding-top: $gl-padding-top;
}
.discussion-notes .disabled-comment {
@@ -237,12 +233,7 @@
.discussion-body,
.diff-file {
.discussion-reply-holder {
- background-color: $white-light;
- padding: 10px 16px;
-
- &.is-replying {
- padding-bottom: $gl-padding;
- }
+ padding-top: $gl-padding;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 81e98f358a8..9d9cbecc958 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -47,7 +47,7 @@ ul.notes {
}
.timeline-entry-inner {
- padding: $gl-padding $gl-btn-padding;
+ padding: $gl-padding 0;
border-bottom: 1px solid $white-normal;
}
@@ -94,12 +94,6 @@ ul.notes {
}
}
- &.note-discussion {
- .timeline-entry-inner {
- padding: $gl-padding 10px;
- }
- }
-
.editing-spinner {
display: none;
}
@@ -352,6 +346,8 @@ ul.notes {
}
.discussion-notes {
+ background-color: $white-light;
+
&:not(:first-child) {
border-top: 1px solid $white-normal;
margin-top: 20px;
@@ -363,10 +359,6 @@ ul.notes {
}
}
- .notes {
- background-color: $white-light;
- }
-
a code {
top: 0;
margin-right: 0;
@@ -647,8 +639,6 @@ ul.notes {
border-bottom: 1px solid $white-normal;
.timeline-entry-inner {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
border-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 42772f13155..ce2f1482456 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -706,8 +706,8 @@ button.mini-pipeline-graph-dropdown-toggle {
// dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
- width: 195px;
- max-width: 195px;
+ width: 240px;
+ max-width: 240px;
.scrollable-menu {
padding: 0;
@@ -750,7 +750,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: #{$ci-action-icon-size - 6};
left: -3px;
position: relative;
- top: -2px;
+ top: -1px;
&.icon-action-stop,
&.icon-action-cancel {
@@ -931,13 +931,11 @@ button.mini-pipeline-graph-dropdown-toggle {
*/
&.dropdown-menu {
transform: translate(-80%, 0);
- min-width: 150px;
@media(min-width: $screen-md-min) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
- min-width: 240px;
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 584b0579b72..9a770d77685 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1121,3 +1121,25 @@ pre.light-well {
padding-top: $gl-padding;
padding-bottom: 37px;
}
+
+.project-ci-body {
+ .incorrect-syntax {
+ font-size: 18px;
+ color: $lint-incorrect-color;
+ }
+
+ .correct-syntax {
+ font-size: 18px;
+ color: $lint-correct-color;
+ }
+}
+
+.project-ci-linter {
+ .ci-editor {
+ height: 400px;
+ }
+
+ .ci-template pre {
+ white-space: pre-wrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 7a8fbfc517d..8cc5c8fc877 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,8 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
- margin-top: 40px;
- color: $almost-black;
+ margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -43,13 +42,18 @@
cursor: pointer;
&.file-open {
- background: $white-normal;
+ background: $link-active-background;
+ }
+
+ &.file-active {
+ font-weight: $gl-font-weight-bold;
}
.ide-file-name {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
+ max-width: inherit;
svg {
vertical-align: middle;
@@ -72,7 +76,10 @@
margin-right: -8px;
}
- &:hover {
+ &:hover,
+ &:focus {
+ background: $link-active-background;
+
.ide-new-btn {
display: block;
}
@@ -290,6 +297,10 @@
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
+
+ .cursors-layer {
+ display: none;
+ }
}
}
@@ -297,14 +308,34 @@
height: 100%;
}
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
+
+.ide-mode-tabs {
border-bottom: 1px solid $white-dark;
- background: $white-light;
+
+ .nav-links {
+ border-bottom: 0;
+
+ li a {
+ padding: $gl-padding-8 $gl-padding;
+ line-height: $gl-btn-line-height;
+ }
+ }
+}
+
+.ide-btn-group {
+ padding: $gl-padding-4 $gl-vert-padding;
}
.ide-status-bar {
+ border-top: 1px solid $white-dark;
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
@@ -398,7 +429,7 @@
}
.branch-container {
- border-left: 4px solid $indigo-700;
+ border-left: 4px solid;
margin-bottom: $gl-bar-padding;
}
@@ -410,7 +441,6 @@
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
- color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
@@ -447,6 +477,8 @@
display: flex;
flex-direction: column;
flex: 1;
+ max-height: 100%;
+ overflow: auto;
}
.multi-file-commit-empty-state-container {
@@ -457,7 +489,7 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- margin-bottom: 12px;
+ margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
@@ -664,8 +696,14 @@
overflow: hidden;
&.nav-only {
+ padding-top: $header-height;
+
+ .with-performance-bar & {
+ padding-top: $header-height + $performance-bar-height;
+ }
+
.flash-container {
- margin-top: $header-height;
+ margin-top: 0;
margin-bottom: 0;
}
@@ -675,7 +713,7 @@
}
.content-wrapper {
- margin-top: $header-height;
+ margin-top: 0;
padding-bottom: 0;
}
@@ -699,11 +737,11 @@
.with-performance-bar .ide.nav-only {
.flash-container {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
}
.content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
+ margin-top: 0;
padding-bottom: 0;
}
@@ -712,14 +750,8 @@
}
&.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
.ide-view {
- height: calc(
- 100vh - #{$header-height + $performance-bar-height + $flash-height}
- );
+ height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
}
}
@@ -763,20 +795,7 @@
.ide-sidebar-link {
padding: $gl-padding-8 $gl-padding;
- background: $indigo-700;
- color: $white-light;
- text-decoration: none;
display: flex;
align-items: center;
-
- &:focus,
- &:hover {
- color: $white-light;
- text-decoration: underline;
- background: $indigo-500;
- }
-
- &:active {
- background: $indigo-800;
- }
+ font-weight: $gl-font-weight-bold;
}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
new file mode 100644
index 00000000000..57b995adb64
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss.orig
@@ -0,0 +1,786 @@
+.project-refs-form,
+.project-refs-target-form {
+ display: inline-block;
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.commit-message {
+ @include str-truncated(250px);
+}
+
+.editable-mode {
+ display: inline-block;
+}
+
+.ide-view {
+ display: flex;
+ height: calc(100vh - #{$header-height});
+ margin-top: 40px;
+ color: $almost-black;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+
+ &.is-collapsed {
+ .ide-file-list {
+ max-width: 250px;
+ }
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+.ide-file-list {
+ flex: 1;
+
+ .file {
+ cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .ide-file-name {
+ flex: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ svg {
+ vertical-align: middle;
+ margin-right: 2px;
+ }
+
+ .loading-container {
+ margin-right: 4px;
+ display: inline-block;
+ }
+ }
+
+ .ide-file-changed-icon {
+ margin-left: auto;
+ }
+
+ .ide-new-btn {
+ display: none;
+ margin-bottom: -4px;
+ margin-right: -8px;
+ }
+
+ &:hover {
+ .ide-new-btn {
+ display: block;
+ }
+ }
+
+ &.folder {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ th {
+ position: sticky;
+ top: 0;
+ }
+}
+
+.file-name,
+.file-col-commit-message {
+ display: flex;
+ overflow: visible;
+ padding: 6px 12px;
+}
+
+.multi-file-loading-container {
+ margin-top: 10px;
+ padding: 10px;
+
+ .animation-container {
+ background: $gray-light;
+
+ div {
+ background: $gray-light;
+ }
+ }
+}
+
+.multi-file-table-col-commit-message {
+ white-space: nowrap;
+ width: 50%;
+}
+
+.multi-file-edit-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ border-left: 1px solid $white-dark;
+ overflow: hidden;
+}
+
+.multi-file-tabs {
+ display: flex;
+ background-color: $white-normal;
+ box-shadow: inset 0 -1px $white-dark;
+
+ > ul {
+ display: flex;
+ overflow-x: auto;
+ }
+
+ li {
+ position: relative;
+ }
+
+ .dropdown {
+ display: flex;
+ margin-left: auto;
+ margin-bottom: 1px;
+ padding: 0 $grid-size;
+ border-left: 1px solid $white-dark;
+ background-color: $white-light;
+
+ &.shadow {
+ box-shadow: 0 0 10px $dropdown-shadow-color;
+ }
+
+ .btn {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ }
+}
+
+.multi-file-tab {
+ @include str-truncated(150px);
+ padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ cursor: pointer;
+
+ svg {
+ vertical-align: middle;
+ }
+
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
+ }
+}
+
+.multi-file-tab-close {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: $border-radius-default;
+ color: $theme-gray-900;
+ transform: translateY(-50%);
+
+ svg {
+ position: relative;
+ top: -1px;
+ }
+
+ &:hover {
+ background-color: $theme-gray-200;
+ }
+
+ &:focus {
+ background-color: $blue-500;
+ color: $white-light;
+ outline: 0;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+}
+
+.multi-file-edit-pane-content {
+ flex: 1;
+ height: 0;
+}
+
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+
+ .monaco-editor .lines-content .cigr {
+ display: none;
+ }
+
+ .monaco-diff-editor.vs {
+ .editor.modified {
+ box-shadow: none;
+ }
+
+ .diagonal-fill {
+ display: none !important;
+ }
+
+ .diffOverview {
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ cursor: ns-resize;
+ }
+
+ .diffViewport {
+ display: none;
+ }
+
+ .char-insert {
+ background-color: $line-added-dark;
+ }
+
+ .char-delete {
+ background-color: $line-removed-dark;
+ }
+
+ .line-numbers {
+ color: $black-transparent;
+ }
+
+ .view-overlays {
+ .line-insert {
+ background-color: $line-added;
+ }
+
+ .line-delete {
+ background-color: $line-removed;
+ }
+ }
+
+ .margin {
+ background-color: $gray-light;
+ border-right: 1px solid $white-normal;
+
+ .line-insert {
+ border-right: 1px solid $line-added-dark;
+ }
+
+ .line-delete {
+ border-right: 1px solid $line-removed-dark;
+ }
+ }
+
+ .margin-view-overlays .insert-sign,
+ .margin-view-overlays .delete-sign {
+ opacity: 0.4;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
+.multi-file-editor-btn-group {
+ padding: $gl-bar-padding $gl-padding;
+ border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+ height: 100%;
+ overflow: scroll;
+
+ .file-content.code {
+ display: flex;
+
+ i {
+ margin-left: -10px;
+ }
+ }
+
+ .line-numbers {
+ min-width: 50px;
+ }
+
+ .file-content,
+ .line-numbers,
+ .blob-content,
+ .code {
+ min-height: 100%;
+ }
+}
+
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
+.multi-file-commit-panel {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ width: 340px;
+ padding: 0;
+ background-color: $gray-light;
+ padding-right: 3px;
+
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+ }
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ &.is-collapsed {
+ width: 60px;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
+ }
+}
+
+.multi-file-commit-panel-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.multi-file-commit-empty-state-container {
+ align-items: center;
+ justify-content: center;
+}
+
+.multi-file-commit-panel-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ border-bottom: 1px solid $white-dark;
+ padding: $gl-btn-padding 0;
+
+ &.is-collapsed {
+ border-bottom: 1px solid $white-dark;
+
+ svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
+ }
+}
+
+.multi-file-commit-panel-header-title {
+ display: flex;
+ flex: 1;
+ padding: 0 $gl-btn-padding;
+
+ svg {
+ margin-right: $gl-btn-padding;
+ }
+}
+
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
+.multi-file-commit-list {
+ flex: 1;
+ overflow: auto;
+ padding: $gl-padding 0;
+ min-height: 60px;
+}
+
+.multi-file-commit-list-item {
+ display: flex;
+ padding: 0;
+ align-items: center;
+
+ .multi-file-discard-btn {
+ display: none;
+ margin-left: auto;
+ color: $gl-link-color;
+ padding: 0 2px;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &:hover {
+ background: $white-normal;
+
+ .multi-file-discard-btn {
+ display: block;
+ }
+ }
+}
+
+.multi-file-addition {
+ fill: $green-500;
+}
+
+.multi-file-modified {
+ fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+ display: flex;
+ flex-direction: column;
+
+ > svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ margin-left: 3px;
+ }
+}
+
+.multi-file-commit-list-path {
+ padding: $grid-size / 2;
+ padding-left: $gl-padding;
+ background: none;
+ border: 0;
+ text-align: left;
+ width: 100%;
+ min-width: 0;
+
+ svg {
+ min-width: 16px;
+ vertical-align: middle;
+ display: inline-block;
+ }
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
+}
+
+.multi-file-commit-list-file-path {
+ @include str-truncated(100%);
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ &:active {
+ text-decoration: none;
+ }
+}
+
+.multi-file-commit-form {
+ padding: $gl-padding;
+ border-top: 1px solid $white-dark;
+
+ .btn {
+ font-size: $gl-font-size;
+ }
+}
+
+.multi-file-commit-message.form-control {
+ height: 160px;
+ resize: none;
+}
+
+.dirty-diff {
+ // !important need to override monaco inline style
+ width: 4px !important;
+ left: 0 !important;
+
+ &-modified {
+ background-color: $blue-500;
+ }
+
+ &-added {
+ background-color: $green-600;
+ }
+
+ &-removed {
+ height: 0 !important;
+ width: 0 !important;
+ bottom: -2px;
+ border-style: solid;
+ border-width: 5px;
+ border-color: transparent transparent transparent $red-500;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100px;
+ height: 1px;
+ background-color: rgba($red-500, 0.5);
+ }
+ }
+}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide {
+ overflow: hidden;
+
+ &.nav-only {
+ .flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+ }
+
+ .alert-wrapper .flash-container .flash-alert:last-child,
+ .alert-wrapper .flash-container .flash-notice:last-child {
+ margin-bottom: 0;
+ }
+
+ .content-wrapper {
+ margin-top: $header-height;
+ padding-bottom: 0;
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $flash-height});
+ }
+ }
+
+ .projects-sidebar {
+ .multi-file-commit-panel-inner-scroll {
+ flex: 1;
+ }
+ }
+ }
+}
+
+.with-performance-bar .ide.nav-only {
+ .flash-container {
+ margin-top: #{$header-height + $performance-bar-height};
+ }
+
+ .content-wrapper {
+ margin-top: #{$header-height + $performance-bar-height};
+ padding-bottom: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $performance-bar-height});
+ }
+
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(
+ 100vh - #{$header-height + $performance-bar-height + $flash-height}
+ );
+ }
+ }
+}
+
+.dragHandle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background-color: $white-dark;
+
+ &.dragright {
+ right: 0;
+ }
+
+ &.dragleft {
+ left: 0;
+ }
+}
+
+.ide-commit-radios {
+ label {
+ font-weight: normal;
+ }
+
+ .help-block {
+ margin-top: 0;
+ line-height: 0;
+ }
+}
+
+.ide-commit-new-branch {
+ margin-left: 25px;
+}
+
+.ide-external-links {
+ p {
+ margin: 0;
+ }
+}
+
+.ide-sidebar-link {
+ padding: $gl-padding-8 $gl-padding;
+ background: $indigo-700;
+ color: $white-light;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+
+ &:focus,
+ &:hover {
+ color: $white-light;
+ text-decoration: underline;
+ background: $indigo-500;
+ }
+
+ &:active {
+ background: $indigo-800;
+ }
+}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index dd0b38970bd..ea302f17d16 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def appearance_params
- params.require(:appearance).permit(
- :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
- :new_project_guidelines, :updated_by
- )
+ params.require(:appearance).permit(allowed_appearance_params)
+ end
+
+ def allowed_appearance_params
+ %i[
+ title
+ description
+ logo
+ logo_cache
+ header_logo
+ header_logo_cache
+ new_project_guidelines
+ updated_by
+ ]
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index cc38608eda5..001f6520093 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -5,7 +5,7 @@ class Admin::GroupsController < Admin::ApplicationController
def index
@groups = Group.with_statistics.with_route
- @groups = @groups.sort(@sort = params[:sort])
+ @groups = @groups.sort_by_attribute(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 156a8e2c515..bfeb5a2d097 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -4,7 +4,7 @@ class Admin::UsersController < Admin::ApplicationController
def index
@users = User.order_name_asc.filter(params[:filter])
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
- @users = @users.sort(@sort = params[:sort])
+ @users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page])
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7f83bd10e93..24651dd392c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -229,10 +229,6 @@ class ApplicationController < ActionController::Base
@event_filter ||= EventFilter.new(filters)
end
- def gitlab_ldap_access(&block)
- Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
- end
-
# JSON for infinite scroll via Pager object
def pager_json(partial, count, locals = {})
html = render_to_string(
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e9bd1689a1e..738a6a5173e 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -4,20 +4,5 @@ module Ci
def show
end
-
- def create
- @content = params[:content]
- @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
- @status = @error.blank?
-
- if @error.blank?
- @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
- @stages = @config_processor.stages
- @builds = @config_processor.builds
- @jobs = @config_processor.jobs
- end
-
- render :show
- end
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index fafb10090ca..56770a17406 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -14,7 +14,7 @@ module GroupTree
end
@groups = @groups.with_selects_for_list(archived: params[:archived])
- .sort(@sort = params[:sort])
+ .sort_by_attribute(@sort = params[:sort])
.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index a21e658fda1..0379f76fc3d 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -88,11 +88,15 @@ module IssuableActions
discussions = Discussion.build_collection(notes, issuable)
- render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self)
+ render json: discussion_serializer.represent(discussions, context: self)
end
private
+ def discussion_serializer
+ DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
+ end
+
def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless issuable.is_a? Spammable
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 03ed5b5310b..839cac3687c 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -212,7 +212,7 @@ module NotesActions
end
def note_serializer
- NoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
+ ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
end
def note_project
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
new file mode 100644
index 00000000000..55011c89886
--- /dev/null
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -0,0 +1,17 @@
+module SendFileUpload
+ def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
+ if attachment
+ redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ send_params.merge!(filename: attachment, disposition: disposition)
+ end
+
+ if file_upload.file_storage?
+ send_file file_upload.path, send_params
+ elsif file_upload.class.proxy_download_enabled?
+ headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
+ head :ok
+ else
+ redirect_to file_upload.url(**redirect_params)
+ end
+ end
+end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 3dbfabcae8a..b9b9b6e4e88 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,5 +1,6 @@
module UploadsActions
include Gitlab::Utils::StrongMemoize
+ include SendFileUpload
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
@@ -26,14 +27,11 @@ module UploadsActions
def show
return render_404 unless uploader&.exists?
- if uploader.file_storage?
- disposition = uploader.image_or_video? ? 'inline' : 'attachment'
- expires_in 0.seconds, must_revalidate: true, private: true
+ expires_in 0.seconds, must_revalidate: true, private: true
- send_file uploader.file.path, disposition: disposition
- else
- redirect_to uploader.url
- end
+ disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+
+ send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
private
@@ -62,19 +60,27 @@ module UploadsActions
end
def build_uploader_from_upload
- return nil unless params[:secret] && params[:filename]
+ return unless uploader = build_uploader
- upload_path = uploader_class.upload_path(params[:secret], params[:filename])
- upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path)
+ upload_paths = uploader.upload_paths(params[:filename])
+ upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths)
upload&.build_uploader
end
def build_uploader_from_params
+ return unless uploader = build_uploader
+
+ uploader.retrieve_from_store!(params[:filename])
+ uploader
+ end
+
+ def build_uploader
+ return unless params[:secret] && params[:filename]
+
uploader = uploader_class.new(model, secret: params[:secret])
- return nil unless uploader.model_valid?
+ return unless uploader.model_valid?
- uploader.retrieve_from_store!(params[:filename])
uploader
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index f210434b2d7..134b0dfc0db 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -17,7 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
- @members = @members.sort(@sort)
+ @members = @members.sort_by_attribute(@sort)
@members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user))
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index cb8771bc97e..6142e75b4c1 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -39,7 +39,7 @@ module Groups
end
def variable_params_attributes
- %i[id key value protected _destroy]
+ %i[id key secret_value protected _destroy]
end
def authorize_admin_build!
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index dbf61a17724..3d27ae18b17 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -51,7 +51,7 @@ class ProfilesController < Profiles::ApplicationController
end
def update_username
- result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute
+ result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
options = if result[:status] == :success
{ notice: "Username successfully changed" }
@@ -72,6 +72,10 @@ class ProfilesController < Profiles::ApplicationController
return render_404 unless @user.can_change_username?
end
+ def username_param
+ @username_param ||= user_params.require(:username)
+ end
+
def user_params
@user_params ||= params.require(:user).permit(
:avatar,
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 0837451cc49..abc283d7aa9 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,6 +1,7 @@
class Projects::ArtifactsController < Projects::ApplicationController
include ExtractsPath
include RendersBlob
+ include SendFileUpload
layout 'project'
before_action :authorize_read_build!
@@ -10,11 +11,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :entry, only: [:file]
def download
- if artifacts_file.file_storage?
- send_file artifacts_file.path, disposition: 'attachment'
- else
- redirect_to artifacts_file.url
- end
+ send_upload(artifacts_file, attachment: artifacts_file.filename)
end
def browse
@@ -45,8 +42,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def raw
- path = Gitlab::Ci::Build::Artifacts::Path
- .new(params[:path])
+ path = Gitlab::Ci::Build::Artifacts::Path.new(params[:path])
send_artifacts_entry(build, path)
end
@@ -75,7 +71,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def validate_artifacts!
- render_404 unless build && build.artifacts?
+ render_404 unless build&.artifacts?
end
def build
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 965cece600e..b7b36f770f5 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -21,17 +21,17 @@ class Projects::BranchesController < Projects::ApplicationController
fetch_branches_by_mode
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
- @merged_branch_names =
- repository.merged_branch_names(@branches.map(&:name))
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
+ @merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
+
+ # n+1: https://gitlab.com/gitlab-org/gitaly/issues/992
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
-
- render
end
+
+ render
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
new file mode 100644
index 00000000000..a2185572a20
--- /dev/null
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -0,0 +1,27 @@
+class Projects::Ci::LintsController < Projects::ApplicationController
+ before_action :authorize_create_pipeline!
+
+ def show
+ end
+
+ def create
+ @content = params[:content]
+ @error = Gitlab::Ci::YamlProcessor.validation_message(@content, yaml_processor_options)
+ @status = @error.blank?
+
+ if @error.blank?
+ @config_processor = Gitlab::Ci::YamlProcessor.new(@content, yaml_processor_options)
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @jobs = @config_processor.jobs
+ end
+
+ render :show
+ end
+
+ private
+
+ def yaml_processor_options
+ { project: @project, sha: project.repository.commit.sha }
+ end
+end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index cba9a53dc4b..8e86af43fee 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -4,8 +4,8 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :check_merge_requests_available!
before_action :merge_request
- before_action :discussion
- before_action :authorize_resolve_discussion!
+ before_action :discussion, only: [:resolve, :unresolve]
+ before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
def resolve
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
@@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
- DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user)
+ DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
.represent(discussion, context: self)
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 8b54ba3ad7c..85e972d9731 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -1,4 +1,6 @@
class Projects::JobsController < Projects::ApplicationController
+ include SendFileUpload
+
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!,
@@ -117,11 +119,17 @@ class Projects::JobsController < Projects::ApplicationController
end
def raw
- build.trace.read do |stream|
- if stream.file?
- send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
- else
- render_404
+ if trace_artifact_file
+ send_upload(trace_artifact_file,
+ send_params: raw_send_params,
+ redirect_params: raw_redirect_params)
+ else
+ build.trace.read do |stream|
+ if stream.file?
+ send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ else
+ render_404
+ end
end
end
end
@@ -136,9 +144,21 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build)
end
+ def raw_send_params
+ { type: 'text/plain; charset=utf-8', disposition: 'inline' }
+ end
+
+ def raw_redirect_params
+ { query: { 'response-content-type' => 'text/plain; charset=utf-8', 'response-content-disposition' => 'inline' } }
+ end
+
+ def trace_artifact_file
+ @trace_artifact_file ||= build.job_artifacts_trace&.file
+ end
+
def build
@build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
+ .present(current_user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 99790b8e7e8..91016f6494e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
- flash[:notice] = "#{@label.title} promoted to group label."
+ flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project), status: 303)
@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
end
def find_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @available_labels ||=
+ LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
end
def authorize_admin_labels!
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index c77f10ef1dd..ee4ed674110 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids
@existing_oids ||= begin
- storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+ project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 941638db427..2515e4b9a17 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -1,6 +1,7 @@
class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsRequest
include WorkhorseRequest
+ include SendFileUpload
skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
@@ -11,25 +12,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
return
end
- send_file lfs_object.file.path, content_type: "application/octet-stream"
+ send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end
def upload_authorize
set_workhorse_internal_api_content_type
- render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
+
+ authorized = LfsObjectUploader.workhorse_authorize
+ authorized.merge!(LfsOid: oid, LfsSize: size)
+
+ render json: authorized
end
def upload_finalize
- unless tmp_filename
- render_lfs_forbidden
- return
- end
-
- if store_file(oid, size, tmp_filename)
+ if store_file!(oid, size)
head 200
else
render plain: 'Unprocessable entity', status: 422
end
+ rescue ActiveRecord::RecordInvalid
+ render_400
+ rescue ObjectStorage::RemoteStoreError
+ render_lfs_forbidden
end
private
@@ -50,38 +54,28 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
params[:size].to_i
end
- def tmp_filename
- name = request.headers['X-Gitlab-Lfs-Tmp']
- return if name.include?('/')
- return unless oid.present? && name.start_with?(oid)
-
- name
- end
+ def store_file!(oid, size)
+ object = LfsObject.find_by(oid: oid, size: size)
+ unless object&.file&.exists?
+ object = create_file!(oid, size)
+ end
- def store_file(oid, size, tmp_file)
- # Define tmp_file_path early because we use it in "ensure"
- tmp_file_path = File.join(LfsObjectUploader.workhorse_upload_path, tmp_file)
+ return unless object
- object = LfsObject.find_or_create_by(oid: oid, size: size)
- file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
- file_exists && link_to_project(object)
- ensure
- FileUtils.rm_f(tmp_file_path)
+ link_to_project!(object)
end
- def move_tmp_file_to_storage(object, path)
- File.open(path) do |f|
- object.file = f
+ def create_file!(oid, size)
+ LfsObject.new(oid: oid, size: size).tap do |object|
+ object.file.store_workhorse_file!(params, :file)
+ object.save!
end
-
- object.file.store!
- object.save
end
- def link_to_project(object)
+ def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project
- object.save
+ object.save!
end
end
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index ff93147d00f..c5a044541f1 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def index
@sort = params[:sort] || 'due_date_asc'
- @milestones = milestones.sort(@sort)
+ @milestones = milestones.sort_by_attribute(@sort)
respond_to do |format|
format.html do
@@ -42,6 +42,10 @@ class Projects::MilestonesController < Projects::ApplicationController
def show
@project_namespace = @project.namespace.becomes(Namespace)
+
+ respond_to do |format|
+ format.html
+ end
end
def create
@@ -70,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
- Milestones::PromoteService.new(project, current_user).execute(milestone)
+ promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "#{milestone.title} promoted to group milestone"
+ flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index b478e7b5e05..fa258f3d9af 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -92,7 +92,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def schedule_params
params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active,
- variables_attributes: [:id, :key, :value, :_destroy] )
+ variables_attributes: [:id, :key, :secret_value, :_destroy] )
end
def authorize_play_pipeline_schedule!
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 557671ab186..73c613b26f3 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -4,41 +4,4 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show
redirect_to project_settings_ci_cd_path(@project, params: params)
end
-
- def update
- Projects::UpdateService.new(project, current_user, update_params).tap do |service|
- if service.execute
- flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
-
- run_autodevops_pipeline(service)
-
- redirect_to project_settings_ci_cd_path(@project)
- else
- render 'show'
- end
- end
- end
-
- private
-
- def run_autodevops_pipeline(service)
- return unless service.run_auto_devops_pipeline?
-
- if @project.empty_repo?
- flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
- return
- end
-
- CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
- flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
- end
-
- def update_params
- params.require(:project).permit(
- :runners_token, :builds_enabled, :build_allow_git_fetch,
- :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
- :auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled]
- )
- end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index e9b4679f94c..cfa5e72af64 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -21,7 +21,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
- @project_members = present_members(@project_members.sort(@sort).page(params[:page]))
+ @project_members = present_members(@project_members.sort_by_attribute(@sort).page(params[:page]))
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index d1719f12072..64954ac9a42 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -5,12 +5,8 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
@project.repository.branches
end
- def create_service_class
- ::ProtectedBranches::CreateService
- end
-
- def update_service_class
- ::ProtectedBranches::UpdateService
+ def service_namespace
+ ::ProtectedBranches
end
def load_protected_ref
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index b51bdf7aa78..9e757a8d25f 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -37,7 +37,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def destroy
- @protected_ref.destroy
+ destroy_service_class.new(@project, current_user).execute(@protected_ref)
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
@@ -47,6 +47,18 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
protected
+ def create_service_class
+ service_namespace::CreateService
+ end
+
+ def update_service_class
+ service_namespace::UpdateService
+ end
+
+ def destroy_service_class
+ service_namespace::DestroyService
+ end
+
def access_level_attributes
%i(access_level id)
end
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
index a5dbd7e46ae..198c938ff35 100644
--- a/app/controllers/projects/protected_tags_controller.rb
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -5,12 +5,8 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@project.repository.tags
end
- def create_service_class
- ::ProtectedTags::CreateService
- end
-
- def update_service_class
- ::ProtectedTags::UpdateService
+ def service_namespace
+ ::ProtectedTags
end
def load_protected_ref
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index a02cc477e08..9bc774b7636 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -2,6 +2,7 @@
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
include BlobHelper
+ include SendFileUpload
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -31,7 +32,7 @@ class Projects::RawController < Projects::ApplicationController
lfs_object = find_lfs_object
if lfs_object && lfs_object.project_allowed_access?(@project)
- send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment'
+ send_upload(lfs_object.file, attachment: @blob.name)
else
render_404
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 2376f469213..48a09e1ddb8 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits"
commits_project_graph_path(@project, @id)
when "badges"
- project_pipelines_settings_path(@project, ref: @id)
+ project_settings_ci_cd_path(@project, ref: @id)
else
project_commits_path(@project, @id)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f14cb5f6a9f..a5ea9ff7ed7 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
else
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
+ rescue Gitlab::HTTP::BlockedUrlError => e
+ { error: true, message: 'Test failed.', service_response: e.message }
end
def success_message
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 259809f3429..d80ef8113aa 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -2,13 +2,24 @@ module Projects
module Settings
class CiCdController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
+ before_action :define_variables
def show
- define_runners_variables
- define_secret_variables
- define_triggers_variables
- define_badges_variables
- define_auto_devops_variables
+ end
+
+ def update
+ Projects::UpdateService.new(project, current_user, update_params).tap do |service|
+ result = service.execute
+ if result[:status] == :success
+ flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
+
+ run_autodevops_pipeline(service)
+
+ redirect_to project_settings_ci_cd_path(@project)
+ else
+ render 'show'
+ end
+ end
end
def reset_cache
@@ -25,16 +36,45 @@ module Projects
private
+ def update_params
+ params.require(:project).permit(
+ :runners_token, :builds_enabled, :build_allow_git_fetch,
+ :build_timeout_human_readable, :build_coverage_regex, :public_builds,
+ :auto_cancel_pending_pipelines, :ci_config_path,
+ auto_devops_attributes: [:id, :domain, :enabled]
+ )
+ end
+
+ def run_autodevops_pipeline(service)
+ return unless service.run_auto_devops_pipeline?
+
+ if @project.empty_repo?
+ flash[:warning] = "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
+ return
+ end
+
+ CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
+ flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+ end
+
+ def define_variables
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ define_auto_devops_variables
+ end
+
def define_runners_variables
@project_runners = @project.runners.ordered
@assignable_runners = current_user.ci_authorized_runners
.assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
+ @shared_runners = ::Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
def define_secret_variables
- @variable = Ci::Variable.new(project: project)
+ @variable = ::Ci::Variable.new(project: project)
.present(current_user: current_user)
@variables = project.variables.order_key_asc
.map { |variable| variable.present(current_user: current_user) }
@@ -42,7 +82,7 @@ module Projects
def define_triggers_variables
@triggers = @project.triggers
- @trigger = Ci::Trigger.new
+ @trigger = ::Ci::Trigger.new
end
def define_badges_variables
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index d06d18c498b..dd9e4a2af3e 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -16,6 +16,10 @@ module Projects
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new
+
+ @protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size }
+ @protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size }
+
load_gon_index
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 7eb509e2e64..517d0b026c2 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -36,6 +36,6 @@ class Projects::VariablesController < Projects::ApplicationController
end
def variable_params_attributes
- %i[id key value protected _destroy]
+ %i[id key secret_value protected _destroy]
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ee197c75764..37f14230196 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -324,7 +324,7 @@ class ProjectsController < Projects::ApplicationController
:avatar,
:build_allow_git_fetch,
:build_coverage_regex,
- :build_timeout_in_minutes,
+ :build_timeout_human_readable,
:resolve_outdated_diff_discussions,
:container_registry_enabled,
:default_branch,
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 8acefd58e77..651b82f04f4 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -42,6 +42,10 @@ class RootController < Dashboard::ProjectsController
redirect_to(dashboard_groups_path)
when 'todos'
redirect_to(dashboard_todos_path)
+ when 'issues'
+ redirect_to(issues_dashboard_path(assignee_id: current_user.id))
+ when 'merge_requests'
+ redirect_to(merge_requests_dashboard_path(assignee_id: current_user.id))
end
end
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index 2c8f21c2400..53b77f5fed9 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -62,6 +62,6 @@ class Admin::ProjectsFinder
def sort(items)
sort = params.fetch(:sort) { 'latest_activity_desc' }
- items.sort(sort)
+ items.sort_by_attribute(sort)
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b2d4f9938ff..61c72aa22a8 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -337,7 +337,7 @@ class IssuableFinder
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
- params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
+ params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end
def by_assignee(items)
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 780c0fdb03e..afd1f824b32 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
if project
if project.group.present?
labels_table = Label.arel_table
+ group_ids = group_ids_for(project.group)
label_ids << Label.where(
- labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+ labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
)
)
@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
label_ids << project.labels
end
end
- elsif only_group_labels?
- label_ids << Label.where(group_id: group_ids)
else
+ if group?
+ group = Group.find(params[:group_id])
+ label_ids << Label.where(group_id: group_ids_for(group))
+ end
+
label_ids << Label.where(group_id: projects.group_ids)
- label_ids << Label.where(project_id: projects.select(:id))
+ label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
end
label_ids
@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_ids
+ # Gets redacted array of group ids
+ # which can include the ancestors and descendants of the requested group.
+ def group_ids_for(group)
strong_memoize(:group_ids) do
- groups_user_can_read_labels(groups_to_include).map(&:id)
+ groups = groups_to_include(group)
+
+ groups_user_can_read_labels(groups).map(&:id)
end
end
- def groups_to_include
- group = Group.find(params[:group_id])
+ def groups_to_include(group)
groups = [group]
- groups += group.ancestors if params[:include_ancestor_groups].present?
- groups += group.descendants if params[:include_descendant_groups].present?
+ groups += group.ancestors if include_ancestor_groups?
+ groups += group.descendants if include_descendant_groups?
groups
end
+ def include_ancestor_groups?
+ params[:include_ancestor_groups]
+ end
+
+ def include_descendant_groups?
+ params[:include_descendant_groups]
+ end
+
def group?
params[:group_id].present?
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 005612ededc..c7d6bc6cfdc 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -124,7 +124,7 @@ class ProjectsFinder < UnionFinder
end
def sort(items)
- params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
+ params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end
def by_archived(projects)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 150f4c7688b..09e2c586f2a 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -119,7 +119,7 @@ class TodosFinder
end
def sort(items)
- params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
+ params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end
def by_action(items)
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c037de33c22..f48db024e3f 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,27 +1,27 @@
module AppearancesHelper
def brand_title
- brand_item&.title.presence || 'GitLab Community Edition'
+ current_appearance&.title.presence || 'GitLab Community Edition'
end
def brand_image
- image_tag(brand_item.logo) if brand_item&.logo?
+ image_tag(current_appearance.logo) if current_appearance&.logo?
end
def brand_text
- markdown_field(brand_item, :description)
+ markdown_field(current_appearance, :description)
end
def brand_new_project_guidelines
- markdown_field(brand_item, :new_project_guidelines)
+ markdown_field(current_appearance, :new_project_guidelines)
end
- def brand_item
+ def current_appearance
@appearance ||= Appearance.current
end
def brand_header_logo
- if brand_item&.header_logo?
- image_tag brand_item.header_logo
+ if current_appearance&.header_logo?
+ image_tag current_appearance.header_logo
else
render 'shared/logo.svg'
end
@@ -29,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
- unless brand_item&.header_logo?
+ unless current_appearance&.header_logo?
render 'shared/logo_type.svg'
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 701be97ee96..86ec500ceb3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -285,6 +285,10 @@ module ApplicationHelper
class_names
end
+ # EE feature: System header and footer, unavailable in CE
+ def system_message_class
+ end
+
# Returns active css class when condition returns true
# otherwise returns nil.
#
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 275e892b2e6..af878bcf9a0 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -53,10 +53,12 @@ module BoardsHelper
end
def board_list_data
+ include_descendant_groups = @group&.present?
+
{
toggle: "dropdown",
- list_labels_path: labels_filter_path(true),
- labels: labels_filter_path(true),
+ list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
+ labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.path,
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 4ddc1dbed49..c86a26ac30f 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -54,9 +54,9 @@ module EmailsHelper
end
def header_logo
- if brand_item && brand_item.header_logo?
+ if current_appearance&.header_logo?
image_tag(
- brand_item.header_logo,
+ current_appearance.header_logo,
style: 'height: 50px'
)
else
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 87ff607dc3f..c4a6a1e4bb3 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -129,13 +129,17 @@ module LabelsHelper
end
end
- def labels_filter_path(only_group_labels = false)
+ def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project
+ options = {}
+ options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
+ options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
+
if project
- project_labels_path(project, :json)
+ project_labels_path(project, :json, options)
elsif @group
- options = { only_group_labels: only_group_labels } if only_group_labels
+ options[:only_group_labels] = only_group_labels if only_group_labels
group_labels_path(@group, :json, options)
else
dashboard_labels_path(:json)
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 40ca666f1bf..9be93fa69ae 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -31,7 +31,7 @@ module NamespacesHelper
def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group)
- group_icon(namespace)
+ group_icon_url(namespace)
else
avatar_icon_for_user(namespace.owner, size)
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 20aed60cb7a..27ed48fdbc7 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -151,16 +151,17 @@ module NotesHelper
}
end
- def notes_data(issuable)
- discussions_path =
- if issuable.is_a?(Issue)
- discussions_project_issue_path(@project, issuable, format: :json)
- else
- discussions_project_merge_request_path(@project, issuable, format: :json)
- end
+ def discussions_path(issuable)
+ if issuable.is_a?(Issue)
+ discussions_project_issue_path(@project, issuable, format: :json)
+ else
+ discussions_project_merge_request_path(@project, issuable, format: :json)
+ end
+ end
+ def notes_data(issuable)
{
- discussionsPath: discussions_path,
+ discussionsPath: discussions_path(issuable),
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
@@ -170,7 +171,6 @@ module NotesHelper
notesPath: notes_url,
totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now.to_i
-
}.to_json
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 18b9bf214a3..a8397b03d63 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,7 +39,10 @@ module PageLayoutHelper
end
def favicon
- Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
+ return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
+ return 'favicon-blue.ico' if Rails.env.development?
+
+ 'favicon.ico'
end
def page_image
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 373dfd457f7..fb523cb865b 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -9,12 +9,14 @@ module PreferencesHelper
# Maps `dashboard` values to more user-friendly option text
DASHBOARD_CHOICES = {
- projects: 'Your Projects (default)',
- stars: 'Starred Projects',
- project_activity: "Your Projects' Activity",
- starred_project_activity: "Starred Projects' Activity",
- groups: "Your Groups",
- todos: "Your Todos"
+ projects: _("Your Projects (default)"),
+ stars: _("Starred Projects"),
+ project_activity: _("Your Projects' Activity"),
+ starred_project_activity: _("Starred Projects' Activity"),
+ groups: _("Your Groups"),
+ todos: _("Your Todos"),
+ issues: _("Assigned Issues"),
+ merge_requests: _("Assigned Merge Requests")
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index f435c80c656..f872990122e 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,4 +1,29 @@
module ServicesHelper
+ def service_event_description(event)
+ case event
+ when "push", "push_events"
+ "Event will be triggered by a push to the repository"
+ when "tag_push", "tag_push_events"
+ "Event will be triggered when a new tag is pushed to the repository"
+ when "note", "note_events"
+ "Event will be triggered when someone adds a comment"
+ when "confidential_note", "confidential_note_events"
+ "Event will be triggered when someone adds a comment on a confidential issue"
+ when "issue", "issue_events"
+ "Event will be triggered when an issue is created/updated/closed"
+ when "confidential_issue", "confidential_issues_events"
+ "Event will be triggered when a confidential issue is created/updated/closed"
+ when "merge_request", "merge_request_events"
+ "Event will be triggered when a merge request is created/updated/merged"
+ when "pipeline", "pipeline_events"
+ "Event will be triggered when a pipeline status changes"
+ when "wiki_page", "wiki_page_events"
+ "Event will be triggered when a wiki page is created/updated"
+ when "commit", "commit_events"
+ "Event will be triggered when a commit is created/updated"
+ end
+ end
+
def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b64be89c181..5e7c20ef51e 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -123,7 +123,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree)
- return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
+ return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir?
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5fe09cea83f..b3f2aeb08ca 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -11,6 +11,15 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil, new_commits: [], existing_commits: [])
+ setup_merge_request_mail(merge_request_id, recipient_id)
+ @new_commits = new_commits
+ @existing_commits = existing_commits
+ @updated_by_user = User.find(updated_by_user_id)
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
+ end
+
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index dcd14c08f3c..2a6406d63c7 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,5 +1,7 @@
class Appearance < ActiveRecord::Base
include CacheMarkdownField
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index ec56cc53aea..760f01f225b 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -36,16 +36,15 @@ module Ci
def external_url(project, job)
return unless external_link?(job)
- full_path_parts = project.full_path_components
- top_level_group = full_path_parts.shift
+ url_project_path = project.full_path.partition('/').last
artifact_path = [
- '-', *full_path_parts, '-',
+ '-', url_project_path, '-',
'jobs', job.id,
'artifacts', path
].join('/')
- "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
+ "#{project.pages_group_url}/#{artifact_path}"
end
def external_link?(job)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1e066b69c6e..4aa65bf4273 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,8 +3,10 @@ module Ci
prepend ArtifactMigratable
include TokenAuthenticatable
include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
include Presentable
include Importable
+ include Gitlab::Utils::StrongMemoize
MissingDependenciesError = Class.new(StandardError)
@@ -23,12 +25,18 @@ module Ci
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
- # The "environment" field for builds is a String, and is the unexpanded name
+ has_one :metadata, class_name: 'Ci::BuildMetadata'
+ delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+
+ ##
+ # The "environment" field for builds is a String, and is the unexpanded name!
+ #
def persisted_environment
- @persisted_environment ||= Environment.find_by(
- name: expanded_environment_name,
- project: project
- )
+ return unless has_environment?
+
+ strong_memoize(:persisted_environment) do
+ Environment.find_by(name: expanded_environment_name, project: project)
+ end
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
@@ -45,6 +53,7 @@ module Ci
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+ scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -81,6 +90,7 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
+ before_create :ensure_metadata
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
@@ -151,6 +161,14 @@ module Ci
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
+
+ before_transition pending: :running do |build|
+ build.ensure_metadata.update_timeout_state
+ end
+ end
+
+ def ensure_metadata
+ metadata || build_metadata(project: project)
end
def detailed_status(current_user)
@@ -198,7 +216,11 @@ module Ci
end
def expanded_environment_name
- ExpandVariables.expand(environment, simple_variables) if environment
+ return unless has_environment?
+
+ strong_memoize(:expanded_environment_name) do
+ ExpandVariables.expand(environment, simple_variables)
+ end
end
def has_environment?
@@ -229,10 +251,6 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
- def timeout
- project.build_timeout
- end
-
def triggered_by?(current_user)
user == current_user
end
@@ -248,31 +266,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
- # Variables whose value does not depend on environment
- def simple_variables
- variables(environment: nil)
- end
-
- # All variables, including those dependent on environment, which could
- # contain unexpanded variables.
- def variables(environment: persisted_environment)
- collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ ##
+ # Variables in the environment name scope.
+ #
+ def scoped_variables(environment: expanded_environment_name)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
- variables.concat(project.deployment_variables(environment: environment)) if has_environment?
+ variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(project.group.secret_variables_for(ref, project)) if project.group
- variables.concat(secret_variables(environment: environment))
+ variables.concat(secret_group_variables)
+ variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
- variables.concat(persisted_environment_variables) if environment
end
+ end
+
+ ##
+ # Variables that do not depend on the environment name.
+ #
+ def simple_variables
+ strong_memoize(:simple_variables) do
+ scoped_variables(environment: nil).to_runner_variables
+ end
+ end
- collection.to_runner_variables
+ ##
+ # All variables, including persisted environment variables.
+ #
+ def variables
+ Gitlab::Ci::Variables::Collection.new
+ .concat(persisted_variables)
+ .concat(scoped_variables)
+ .concat(persisted_environment_variables)
+ .to_runner_variables
+ end
+
+ ##
+ # Regular Ruby hash of scoped variables, without duplicates that are
+ # possible to be present in an array of hashes returned from `variables`.
+ #
+ def scoped_variables_hash
+ scoped_variables.to_hash
end
def features
@@ -365,13 +404,19 @@ module Ci
project.running_or_pending_build_count(force: true)
end
+ def browsable_artifacts?
+ artifacts_metadata?
+ end
+
def artifacts_metadata_entry(path, **options)
- metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
- artifacts_metadata.path,
- path,
- **options)
+ artifacts_metadata.use_file do |metadata_path|
+ metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
+ metadata_path,
+ path,
+ **options)
- metadata.to_entry
+ metadata.to_entry
+ end
end
def erase_artifacts!
@@ -443,9 +488,14 @@ module Ci
end
end
- def secret_variables(environment: persisted_environment)
+ def secret_group_variables
+ return [] unless project.group
+
+ project.group.secret_variables_for(ref, project)
+ end
+
+ def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
- .map(&:to_runner_variable)
end
def steps
@@ -542,6 +592,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+ def persisted_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted?
+
+ variables
+ .append(key: 'CI_JOB_ID', value: id.to_s)
+ .append(key: 'CI_JOB_TOKEN', value: token, public: false)
+ .append(key: 'CI_BUILD_ID', value: id.to_s)
+ .append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+ .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
+ .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ end
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
@@ -550,16 +615,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
- variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
- variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
- variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
- variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
- variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -567,23 +627,8 @@ module Ci
end
end
- def persisted_environment_variables
- Gitlab::Ci::Variables::Collection.new.tap do |variables|
- return variables unless persisted_environment
-
- variables.concat(persisted_environment.predefined_variables)
-
- # Here we're passing unexpanded environment_url for runner to expand,
- # and we need to make sure that CI_ENVIRONMENT_NAME and
- # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
- variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
- end
- end
-
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
- variables.append(key: 'CI_BUILD_ID', value: id.to_s)
- variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
@@ -596,6 +641,19 @@ module Ci
end
end
+ def persisted_environment_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted? && persisted_environment.present?
+
+ variables.concat(persisted_environment.predefined_variables)
+
+ # Here we're passing unexpanded environment_url for runner to expand,
+ # and we need to make sure that CI_ENVIRONMENT_NAME and
+ # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+ variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
+ end
+ end
+
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
new file mode 100644
index 00000000000..96762f8845c
--- /dev/null
+++ b/app/models/ci/build_metadata.rb
@@ -0,0 +1,35 @@
+module Ci
+ # The purpose of this class is to store Build related data that can be disposed.
+ # Data that should be persisted forever, should be stored with Ci::Build model.
+ class BuildMetadata < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+ include Presentable
+ include ChronicDurationAttribute
+
+ self.table_name = 'ci_builds_metadata'
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :project
+
+ validates :build, presence: true
+ validates :project, presence: true
+
+ chronic_duration_attr_reader :timeout_human_readable, :timeout
+
+ enum timeout_source: {
+ unknown_timeout_source: 1,
+ project_timeout_source: 2,
+ runner_timeout_source: 3
+ }
+
+ def update_timeout_state
+ return unless build.runner.present?
+
+ project_timeout = project&.build_timeout
+ timeout = [project_timeout, build.runner.maximum_timeout].compact.min
+ timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
+
+ update(timeout: timeout, timeout_source: timeout_source)
+ end
+ end
+end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 1dd0e050ba9..62d768cc6cf 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -6,6 +6,8 @@ module Ci
belongs_to :group
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: {
scope: :group_id,
message: "(%{value}) has already been taken"
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0a599f72bc7..df57b4f65e3 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -1,5 +1,7 @@
module Ci
class JobArtifact < ActiveRecord::Base
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
extend Gitlab::Ci::Model
belongs_to :project
@@ -7,9 +9,11 @@ module Ci
before_save :set_size, if: :file_changed?
+ scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+
mount_uploader :file, JobArtifactUploader
- delegate :open, :exists?, to: :file
+ delegate :exists?, :open, to: :file
enum file_type: {
archive: 1,
@@ -21,6 +25,10 @@ module Ci
self.where(project: project).sum(:size)
end
+ def local_store?
+ [nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
+ end
+
def set_size
self.size = file.size
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 44f9bdf111e..434b9b64c65 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -6,6 +6,7 @@ module Ci
include AfterCommitQueue
include Presentable
include Gitlab::OptimisticLocking
+ include Gitlab::Utils::StrongMemoize
belongs_to :project, inverse_of: :pipelines
belongs_to :user
@@ -361,21 +362,23 @@ module Ci
def stage_seeds
return [] unless config_processor
- @stage_seeds ||= config_processor.stage_seeds(self)
+ strong_memoize(:stage_seeds) do
+ seeds = config_processor.stages_attributes.map do |attributes|
+ Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
+ end
+
+ seeds.select(&:included?)
+ end
end
def seeds_size
- @seeds_size ||= stage_seeds.sum(&:size)
+ stage_seeds.sum(&:size)
end
def has_kubernetes_active?
project.deployment_platform&.active?
end
- def has_stage_seeds?
- stage_seeds.any?
- end
-
def has_warnings?
builds.latest.failed_but_allowed.any?
end
@@ -388,6 +391,9 @@ module Ci
end
end
+ ##
+ # TODO, setting yaml_errors should be moved to the pipeline creation chain.
+ #
def config_processor
return unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@@ -472,6 +478,14 @@ module Ci
end
end
+ def protected_ref?
+ strong_memoize(:protected_ref) { project.protected_for?(ref) }
+ end
+
+ def legacy_trigger
+ strong_memoize(:legacy_trigger) { trigger_requests.first }
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PIPELINE_ID', value: id.to_s)
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index af989fb14b4..03df4e3e638 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline_schedule
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_schedule_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 7173f88f1c7..5a4c56ec0dc 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -3,12 +3,13 @@ module Ci
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
include RedisCacheable
+ include ChronicDurationAttribute
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
- FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -51,6 +52,12 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
+ chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
+
+ validates :maximum_timeout, allow_nil: true,
+ numericality: { greater_than_or_equal_to: 600,
+ message: 'needs to be at least 10 minutes' }
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7c71291de84..452cb910bca 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -6,6 +6,8 @@ module Ci
belongs_to :project
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: {
scope: [:project_id, :environment_scope],
message: "(%{value}) has already been taken"
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 49eb069016a..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -10,6 +10,7 @@ module Clusters
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner
}.freeze
+ DEFAULT_ENVIRONMENT = '*'.freeze
belongs_to :user
@@ -50,6 +51,11 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
+ scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
+ scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
+ scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
+
+ scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
if provider
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 7b7c8eac773..8f3eb75bfa9 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -4,6 +4,8 @@ module Clusters
extend ActiveSupport::Concern
included do
+ scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
+
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
state :errored, value: -1
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cceae5efb72..3f7f36e83c0 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -32,7 +32,8 @@ class Commit
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field)
- context = { pipeline: :single_line, project: self.project }
+ pipeline = field == :description ? :commit_description : :single_line
+ context = { pipeline: pipeline, project: self.project }
context[:author] = self.author if self.author
context
@@ -175,7 +176,7 @@ class Commit
if safe_message.blank?
no_commit_message
else
- safe_message.split("\n", 2).first
+ safe_message.split(/[\r\n]/, 2).first
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 9fb5b7efec6..3469d5d795c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end
def group_name
- name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip
+ name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
end
def failed_but_allowed?
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 318df11727e..7677891b9ce 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -3,6 +3,7 @@ module Avatarable
included do
prepend ShadowMethods
+ include ObjectStorage::BackgroundMove
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
new file mode 100644
index 00000000000..593a9b3d71d
--- /dev/null
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -0,0 +1,39 @@
+module ChronicDurationAttribute
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def chronic_duration_attr_reader(virtual_attribute, source_attribute)
+ define_method(virtual_attribute) do
+ chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
+ end
+ end
+
+ def chronic_duration_attr_writer(virtual_attribute, source_attribute, parameters = {})
+ chronic_duration_attr_reader(virtual_attribute, source_attribute)
+
+ define_method("#{virtual_attribute}=") do |value|
+ chronic_duration_attributes[virtual_attribute] = value.presence || parameters[:default].presence.to_s
+
+ begin
+ new_value = value.present? ? ChronicDuration.parse(value).to_i : parameters[:default].presence
+ assign_attributes(source_attribute => new_value)
+ rescue ChronicDuration::DurationParseError
+ # ignore error as it will be caught by validation
+ end
+ end
+
+ validates virtual_attribute, allow_nil: true, duration: true
+ end
+
+ alias_method :chronic_duration_attr, :chronic_duration_attr_writer
+ end
+
+ def chronic_duration_attributes
+ @chronic_duration_attributes ||= {}
+ end
+
+ def output_chronic_duration_attribute(source_attribute)
+ value = attributes[source_attribute.to_s]
+ ChronicDuration.output(value, format: :short) if value
+ end
+end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index faa94204e33..52851b3d0b2 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -1,16 +1,24 @@
module DeploymentPlatform
- # EE would override this and utilize the extra argument
+ # EE would override this and utilize environment argument
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
- @deployment_platform ||=
- find_cluster_platform_kubernetes ||
- find_kubernetes_service_integration ||
- build_cluster_and_deployment_platform
+ @deployment_platform ||= {}
+
+ @deployment_platform[environment] ||= find_deployment_platform(environment)
end
private
- def find_cluster_platform_kubernetes
- clusters.find_by(enabled: true)&.platform_kubernetes
+ def find_deployment_platform(environment)
+ find_cluster_platform_kubernetes(environment: environment) ||
+ find_kubernetes_service_integration ||
+ build_cluster_and_deployment_platform
+ end
+
+ # EE would override this and utilize environment argument
+ def find_cluster_platform_kubernetes(environment: nil)
+ clusters.enabled.default_environment
+ .last&.platform_kubernetes
end
def find_kubernetes_service_integration
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5a566f3ac02..b45395343cc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -137,7 +137,7 @@ module Issuable
fuzzy_search(query, [:title, :description])
end
- def sort(method, excluded_labels: [])
+ def sort_by_attribute(method, excluded_labels: [])
sorted =
case method.to_s
when 'downvotes_desc' then order_downvotes_desc
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index caf8afa97f9..5130ecec472 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -45,11 +45,11 @@ module Milestoneish
end
def sorted_issues(user)
- issues_visible_to_user(user).preload_associations.sort('label_priority')
+ issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
end
def sorted_merge_requests
- merge_requests.sort('label_priority')
+ merge_requests.sort_by_attribute('label_priority')
end
def upcoming?
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index c2e0a5fa126..89a74b7dcb1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -27,6 +27,10 @@ class DeployKey < Key
self.private?
end
+ def user
+ super || User.ghost
+ end
+
def has_access_to?(project)
deploy_keys_project_for(project).present?
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 17a198d52c7..3805f6cf857 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -52,12 +52,12 @@ class Event < ActiveRecord::Base
belongs_to :target, -> {
# If the association for "target" defines an "author" association we want to
# eager-load this so Banzai & friends don't end up performing N+1 queries to
- # get the authors of notes, issues, etc.
- if reflections['events'].active_record.reflect_on_association(:author)
- includes(:author)
- else
- self
+ # get the authors of notes, issues, etc. (likewise for "noteable").
+ incs = %i(author noteable).select do |a|
+ reflections['events'].active_record.reflect_on_association(a)
end
+
+ incs.reduce(self) { |obj, a| obj.includes(a) }
}, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload
diff --git a/app/models/group.rb b/app/models/group.rb
index d99af79b5fe..3cfe21ac93b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -53,7 +53,7 @@ class Group < Namespace
Gitlab::Database.postgresql?
end
- def sort(method)
+ def sort_by_attribute(method)
if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
# pass a string to avoid AR adding the table name
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b6dd39b860b..ec072882cc9 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -7,6 +7,7 @@ class ProjectHook < WebHook
:issue_hooks,
:confidential_issue_hooks,
:note_hooks,
+ :confidential_note_hooks,
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7bfc45c1f43..13abc6c1a0d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
+
+ before_transition closed: :opened do |issue|
+ issue.closed_at = nil
+ issue.closed_by = nil
+ end
end
class << self
@@ -110,7 +116,7 @@ class Issue < ActiveRecord::Base
'project_id'
end
- def self.sort(method, excluded_labels: [])
+ def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index b444812a4cf..b7de46fa202 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -1,15 +1,30 @@
class LfsObject < ActiveRecord::Base
+ include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
+
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :lfs_objects_projects
+ scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
+
validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader
+ before_save :update_file_store
+
+ def update_file_store
+ self.file_store = file.object_store
+ end
+
def project_allowed_access?(project)
projects.exists?(project.lfs_storage_project.id)
end
+ def local_store?
+ [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
+ end
+
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
diff --git a/app/models/member.rb b/app/models/member.rb
index e1a32148538..eac4a22a03f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -96,7 +96,7 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query))
end
- def sort(method)
+ def sort_by_attribute(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
when 'access_level_desc' then reorder(access_level: :desc)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7e6d89ec9c7..91d8be5559b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -536,18 +536,25 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff(true)
end
+ def viewable_diffs
+ @viewable_diffs ||= merge_request_diffs.viewable.to_a
+ end
+
def merge_request_diff_for(diff_refs_or_sha)
- @merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
- diffs = merge_request_diffs.viewable
- h[diff_refs_or_sha] =
- if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
- diffs.find_by_diff_refs(diff_refs_or_sha)
- else
- diffs.find_by(head_commit_sha: diff_refs_or_sha)
- end
- end
+ matcher =
+ if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
+ {
+ 'start_commit_sha' => diff_refs_or_sha.start_sha,
+ 'head_commit_sha' => diff_refs_or_sha.head_sha,
+ 'base_commit_sha' => diff_refs_or_sha.base_sha
+ }
+ else
+ { 'head_commit_sha' => diff_refs_or_sha }
+ end
- @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha]
+ viewable_diffs.find do |diff|
+ diff.attributes.slice(*matcher.keys) == matcher
+ end
end
def version_params_for(diff_refs)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e7d397f40f5..dafae58d121 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -138,7 +138,7 @@ class Milestone < ActiveRecord::Base
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end
- def self.sort(method)
+ def self.sort_by_attribute(method)
case method.to_s
when 'due_date_asc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
diff --git a/app/models/note.rb b/app/models/note.rb
index 787a80f0196..e426f84832b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -268,6 +268,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
+ def confidential?
+ noteable.try(:confidential?)
+ end
+
def editable?
!system?
end
@@ -313,6 +317,10 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
+ def can_create_notification?
+ true
+ end
+
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
@@ -379,12 +387,15 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
- key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
+ Gitlab::EtagCaching::Store.new.touch(etag_key)
+ end
+
+ def etag_key
+ Gitlab::Routing.url_helpers.project_noteable_notes_path(
project,
target_type: noteable_type.underscore,
target_id: noteable_id
)
- Gitlab::EtagCaching::Store.new.touch(key)
end
def touch(*args)
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index e95655e19f8..b3ffad00a07 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -48,7 +48,7 @@ class NotificationRecipient
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch, :participating
- !excluded_watcher_action?
+ !action_excluded?
when :mention
@type == :mention
else
@@ -96,13 +96,22 @@ class NotificationRecipient
end
end
+ def action_excluded?
+ excluded_watcher_action? || excluded_participating_action?
+ end
+
def excluded_watcher_action?
- return false unless @custom_action
- return false if notification_level == :custom
+ return false unless @custom_action && notification_level == :watch
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
+ def excluded_participating_action?
+ return false unless @custom_action && notification_level == :participating
+
+ NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
+ end
+
private
def read_ability
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 245f8dddcf9..f6d9b0215fc 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -33,6 +33,7 @@ class NotificationSetting < ActiveRecord::Base
:close_issue,
:reassign_issue,
:new_merge_request,
+ :push_to_merge_request,
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
@@ -41,10 +42,14 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
].freeze
- EXCLUDED_WATCHER_EVENTS = [
+ EXCLUDED_PARTICIPATING_EVENTS = [
:success_pipeline
].freeze
+ EXCLUDED_WATCHER_EVENTS = [
+ :push_to_merge_request
+ ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
+
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
diff --git a/app/models/project.rb b/app/models/project.rb
index 6a420663644..1b29cbf28d2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -21,6 +21,7 @@ class Project < ActiveRecord::Base
include Gitlab::SQL::Pattern
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
+ include ChronicDurationAttribute
extend Gitlab::ConfigHelper
@@ -325,6 +326,12 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
+ chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
+
+ validates :build_timeout, allow_nil: true,
+ numericality: { greater_than_or_equal_to: 600,
+ message: 'needs to be at least 10 minutes' }
+
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
@@ -436,7 +443,7 @@ class Project < ActiveRecord::Base
Gitlab::VisibilityLevel.options
end
- def sort(method)
+ def sort_by_attribute(method)
case method.to_s
when 'storage_size_desc'
# storage_size is a joined column so we need to
@@ -566,9 +573,7 @@ class Project < ActiveRecord::Base
def add_import_job
job_id =
if forked?
- RepositoryForkWorker.perform_async(id,
- forked_from_project.repository_storage_path,
- forked_from_project.disk_path)
+ RepositoryForkWorker.perform_async(id)
elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id)
@@ -632,7 +637,7 @@ class Project < ActiveRecord::Base
end
def create_or_update_import_data(data: nil, credentials: nil)
- return unless import_url.present? && valid_import_url?
+ return if data.nil? && credentials.nil?
project_import_data = import_data || build_import_data
if data
@@ -1068,6 +1073,16 @@ class Project < ActiveRecord::Base
end
end
+ # This will return all `lfs_objects` that are accessible to the project.
+ # So this might be `self.lfs_objects` if the project is not part of a fork
+ # network, or it is the base of the fork network.
+ #
+ # TODO: refactor this to get the correct lfs objects when implementing
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+ def all_lfs_objects
+ lfs_storage_project.lfs_objects
+ end
+
def personal?
!group
end
@@ -1301,14 +1316,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- def build_timeout_in_minutes
- build_timeout / 60
- end
-
- def build_timeout_in_minutes=(value)
- self.build_timeout = value.to_i * 60
- end
-
def open_issues_count
Projects::OpenIssuesCountService.new(self).count
end
@@ -1346,20 +1353,19 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path)
end
- def pages_url
- subdomain, _, url_path = full_path.partition('/')
-
- # The hostname always needs to be in downcased
- # All web servers convert hostname to lowercase
- host = "#{subdomain}.#{Settings.pages.host}".downcase
-
+ def pages_group_url
# The host in URL always needs to be downcased
- url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
- "#{prefix}#{subdomain}."
+ Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
+ "#{prefix}#{pages_subdomain}."
end.downcase
+ end
+
+ def pages_url
+ url = pages_group_url
+ url_path = full_path.partition('/').last
# If the project path is the same as host, we serve it as group page
- return url if host == url_path
+ return url if url == "#{Settings.pages.protocol}://#{url_path}"
"#{url}/#{url_path}"
end
@@ -1481,6 +1487,7 @@ class Project < ActiveRecord::Base
remove_import_jid
update_project_counter_caches
after_create_default_branch
+ refresh_markdown_cache!
end
def update_project_counter_caches
@@ -1545,8 +1552,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
- def add_export_job(current_user:, params: {})
- job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
+ def add_export_job(current_user:, after_export_strategy: nil, params: {})
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
@@ -1572,6 +1579,8 @@ class Project < ActiveRecord::Base
def export_status
if export_in_progress?
:started
+ elsif after_export_in_progress?
+ :after_export_action
elsif export_project_path
:finished
else
@@ -1583,12 +1592,22 @@ class Project < ActiveRecord::Base
import_export_shared.active_export_count > 0
end
+ def after_export_in_progress?
+ import_export_shared.after_export_in_progress?
+ end
+
def remove_exports
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
+ def remove_exported_project_file
+ return unless export_project_path.present?
+
+ FileUtils.rm_f(export_project_path)
+ end
+
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index dab0ea1a681..7591ab4f478 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end
end
+ def confidential_issue_channel
+ properties['confidential_issue_channel'].presence || properties['issue_channel']
+ end
+
+ def confidential_note_channel
+ properties['confidential_note_channel'].presence || properties['note_channel']
+ end
+
def self.supported_events
- %w[push issue confidential_issue merge_request note tag_push
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message
- channel_name = get_channel_field(object_kind).presence || channel
+ event_type = data[:event_type] || object_kind
+
+ channel_name = get_channel_field(event_type).presence || channel
opts = {}
opts[:channel] = channel_name if channel_name
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 017a9b2df6e..26cbfd784ad 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -36,7 +36,7 @@ class GemnasiumService < Service
after: data[:after],
token: token,
api_key: api_key,
- repo: project.repository.path_to_repo
+ repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9
)
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index f31c3f02af2..dce878e485f 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -46,7 +46,7 @@ class HipchatService < Service
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push pipeline)
+ %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
end
def execute(data)
diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb
index 20532527346..31de204d824 100644
--- a/app/models/redirect_route.rb
+++ b/app/models/redirect_route.rb
@@ -17,32 +17,4 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%")
end
-
- scope :permanent, -> do
- if column_permanent_exists?
- where(permanent: true)
- else
- none
- end
- end
-
- scope :temporary, -> do
- if column_permanent_exists?
- where(permanent: [false, nil])
- else
- all
- end
- end
-
- default_value_for :permanent, false
-
- def permanent=(value)
- if self.class.column_permanent_exists?
- super
- end
- end
-
- def self.column_permanent_exists?
- ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
- end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 42f1ac43e29..fd1afafe4df 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -93,10 +93,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
- def create_hooks
- Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path)
- end
-
def commit(ref = 'HEAD')
return nil unless exists?
return ref if ref.is_a?(::Commit)
@@ -253,13 +249,13 @@ class Repository
end
def diverging_commit_counts(branch)
- root_ref_hash = raw_repository.commit(root_ref).id
+ @root_ref_hash ||= raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind, number_commits_ahead =
raw_repository.count_commits_between(
- root_ref_hash,
+ @root_ref_hash,
branch.dereferenced_target.sha,
left_right: true,
max_count: MAX_DIVERGING_COUNT)
diff --git a/app/models/route.rb b/app/models/route.rb
index 07d96c21cf1..2d609920051 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -10,8 +10,6 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- validate :ensure_permanent_paths, if: :path_changed?
-
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
@@ -45,7 +43,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants.
- route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
+ route.create_redirect(old_path) if attributes[:path]
end
end
end
@@ -55,31 +53,17 @@ class Route < ActiveRecord::Base
end
def conflicting_redirects
- RedirectRoute.temporary.matching_path_and_descendants(path)
+ RedirectRoute.matching_path_and_descendants(path)
end
- def create_redirect(path, permanent: false)
- RedirectRoute.create(source: source, path: path, permanent: permanent)
+ def create_redirect(path)
+ RedirectRoute.create(source: source, path: path)
end
private
def create_redirect_for_old_path
- create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
- end
-
- def permanent_redirect?
- source_type != "Project"
- end
-
- def ensure_permanent_paths
- return if path.nil?
-
- errors.add(:path, "has been taken before") if conflicting_redirect_exists?
- end
-
- def conflicting_redirect_exists?
- RedirectRoute.permanent.matching_path_and_descendants(path).exists?
+ create_redirect(path_was) if path_changed?
end
def delete_conflicting_orphaned_routes
diff --git a/app/models/service.rb b/app/models/service.rb
index 1dcb79157a2..e9b6f005aec 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -14,6 +14,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
+ default_value_for :confidential_note_events, true
default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
@@ -42,6 +43,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
+ scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
@@ -168,8 +170,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
- def #{arg}
- properties['#{arg}']
+ unless method_defined?(arg)
+ def #{arg}
+ properties['#{arg}']
+ end
end
def #{arg}=(value)
@@ -273,6 +277,7 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template)
service = template.dup
+ service.active = false unless service.valid?
service.template = false
service.project_id = project_id
service
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8afacd188e0..a2ab405fdbe 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -50,7 +50,7 @@ class Todo < ActiveRecord::Base
# Priority sorting isn't displayed in the dropdown, because we don't show
# milestones, but still show something if the user has a URL with that
# selected.
- def sort(method)
+ def sort_by_attribute(method)
sorted =
case method.to_s
when 'priority', 'label_priority' then order_by_labels_priority
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 99ad37dc892..cf71a7b76fc 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -9,6 +9,8 @@ class Upload < ActiveRecord::Base
validates :model, presence: true
validates :uploader, presence: true
+ scope :with_files_stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
@@ -21,6 +23,7 @@ class Upload < ActiveRecord::Base
end
def absolute_path
+ raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
return path unless relative_path?
uploader_class.absolute_path(self)
@@ -30,11 +33,11 @@ class Upload < ActiveRecord::Base
self.checksum = nil
return unless checksummable?
- self.checksum = self.class.hexdigest(absolute_path)
+ self.checksum = Digest::SHA256.file(absolute_path).hexdigest
end
- def build_uploader
- uploader_class.new(model, mount_point, **uploader_context).tap do |uploader|
+ def build_uploader(mounted_as = nil)
+ uploader_class.new(model, mounted_as || mount_point).tap do |uploader|
uploader.upload = self
uploader.retrieve_from_store!(identifier)
end
@@ -51,6 +54,12 @@ class Upload < ActiveRecord::Base
}.compact
end
+ def local?
+ return true if store.nil?
+
+ store == ObjectStorage::Store::LOCAL
+ end
+
private
def delete_file!
@@ -61,10 +70,6 @@ class Upload < ActiveRecord::Base
checksum.nil? && local? && exist?
end
- def local?
- true
- end
-
def foreground_checksummable?
checksummable? && size <= CHECKSUM_THRESHOLD
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b8c55205ab8..7b6857a0d34 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -82,11 +82,8 @@ class User < ActiveRecord::Base
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
- has_many :keys, -> do
- type = Key.arel_table[:type]
- where(type.not_eq('DeployKey').or(type.eq(nil)))
- end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -167,12 +164,15 @@ class User < ActiveRecord::Base
before_validation :sanitize_attrs
before_validation :set_notification_email, if: :email_changed?
+ before_save :set_notification_email, if: :email_changed? # in case validation is skipped
before_validation :set_public_email, if: :public_email_changed?
+ before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
+ before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
@@ -187,7 +187,7 @@ class User < ActiveRecord::Base
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos, :issues, :merge_requests]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
@@ -259,7 +259,7 @@ class User < ActiveRecord::Base
end
end
- def sort(method)
+ def sort_by_attribute(method)
order_method = method || 'id_desc'
case order_method.to_s
@@ -411,7 +411,6 @@ class User < ActiveRecord::Base
unique_internal(where(ghost: true), 'ghost', email) do |u|
u.bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
u.name = 'Ghost User'
- u.notification_email = email
end
end
end
@@ -623,9 +622,7 @@ class User < ActiveRecord::Base
end
def owned_projects
- @owned_projects ||=
- Project.where('namespace_id IN (?) OR namespace_id = ?',
- owned_groups.select(:id), namespace.id).joins(:namespace)
+ @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects")
end
# Returns projects which user can admin issues on (for example to move an issue to that project).
@@ -1196,6 +1193,15 @@ class User < ActiveRecord::Base
private
+ def owned_projects_union
+ Gitlab::SQL::Union.new([
+ Project.where(namespace: namespace),
+ Project.joins(:project_authorizations)
+ .where("projects.namespace_id <> ?", namespace.id)
+ .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER })
+ ], remove_duplicates: false)
+ end
+
def ci_projects_union
scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
groups = groups_projects.where(members: scope)
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 3f6d7d04667..e86d1c8f98e 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -2,20 +2,6 @@ class IssuablePolicy < BasePolicy
delegate { @subject.project }
condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? }
-
- # We aren't checking `:read_issue` or `:read_merge_request` in this case
- # because it could be possible for a user to see an issuable-iid
- # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be allowed
- # to read the actual issue after a more expensive `:read_issue` check.
- #
- # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
- condition(:visible_to_user, score: 4) do
- Project.where(id: @subject.project)
- .public_or_visible_to_user(@user)
- .with_feature_available_for_user(@subject, @user)
- .any?
- end
-
condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) }
desc "User is the assignee or author"
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index ed499511999..263c6e3039c 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -17,6 +17,4 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue
prevent :admin_issue
end
-
- rule { can?(:read_issue) | visible_to_user }.enable :read_issue_iid
end
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index e003376d219..c3fe857f8a2 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -1,3 +1,2 @@
class MergeRequestPolicy < IssuablePolicy
- rule { can?(:read_merge_request) | visible_to_user }.enable :read_merge_request_iid
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 57ab0c23dcd..b1ed034cd00 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -66,6 +66,22 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
+ # We aren't checking `:read_issue` or `:read_merge_request` in this case
+ # because it could be possible for a user to see an issuable-iid
+ # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
+ # allowed to read the actual issue after a more expensive `:read_issue`
+ # check. These checks are intended to be used alongside
+ # `:read_project_for_iids`.
+ #
+ # `:read_issue` & `:read_issue_iid` could diverge in gitlab-ee.
+ condition(:issues_visible_to_user, score: 4) do
+ @subject.feature_available?(:issues, @user)
+ end
+
+ condition(:merge_requests_visible_to_user, score: 4) do
+ @subject.feature_available?(:merge_requests, @user)
+ end
+
features = %w[
merge_requests
issues
@@ -81,6 +97,10 @@ class ProjectPolicy < BasePolicy
condition(:"#{f}_disabled", score: 32) { !feature_available?(f.to_sym) }
end
+ # `:read_project` may be prevented in EE, but `:read_project_for_iids` should
+ # not.
+ rule { guest | admin }.enable :read_project_for_iids
+
rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access
rule { developer }.enable :developer_access
@@ -150,6 +170,7 @@ class ProjectPolicy < BasePolicy
# where we enable or prevent it based on other coditions.
rule { (~anonymous & public_project) | internal_access }.policy do
enable :public_user_access
+ enable :read_project_for_iids
end
rule { can?(:public_user_access) }.policy do
@@ -255,7 +276,11 @@ class ProjectPolicy < BasePolicy
end
rule { anonymous & ~public_project }.prevent_all
- rule { public_project }.enable(:public_access)
+
+ rule { public_project }.policy do
+ enable :public_access
+ enable :read_project_for_iids
+ end
rule { can?(:public_access) }.policy do
enable :read_project
@@ -305,6 +330,14 @@ class ProjectPolicy < BasePolicy
enable :update_pipeline
end
+ rule do
+ (can?(:read_project_for_iids) & issues_visible_to_user) | can?(:read_issue)
+ end.enable :read_issue_iid
+
+ rule do
+ (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
+ end.enable :read_merge_request_iid
+
private
def team_member?
diff --git a/app/policies/protected_branch_policy.rb b/app/policies/protected_branch_policy.rb
new file mode 100644
index 00000000000..1a7faa4db40
--- /dev/null
+++ b/app/policies/protected_branch_policy.rb
@@ -0,0 +1,9 @@
+class ProtectedBranchPolicy < BasePolicy
+ delegate { @subject.project }
+
+ rule { can?(:admin_project) }.policy do
+ enable :create_protected_branch
+ enable :update_protected_branch
+ enable :destroy_protected_branch
+ end
+end
diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb
new file mode 100644
index 00000000000..5048f967ea8
--- /dev/null
+++ b/app/presenters/ci/build_metadata_presenter.rb
@@ -0,0 +1,18 @@
+module Ci
+ class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
+ TIMEOUT_SOURCES = {
+ unknown_timeout_source: nil,
+ project_timeout_source: 'project',
+ runner_timeout_source: 'runner'
+ }.freeze
+
+ presents :metadata
+
+ def timeout_source
+ return unless metadata.timeout_source?
+
+ TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
+ metadata.timeout_source
+ end
+ end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 69d46f5ec14..ca4480fe2b1 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
+ expose :metadata, using: BuildMetadataEntity
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb
new file mode 100644
index 00000000000..f16f3badffa
--- /dev/null
+++ b/app/serializers/build_metadata_entity.rb
@@ -0,0 +1,6 @@
+class BuildMetadataEntity < Grape::Entity
+ expose :timeout_human_readable
+ expose :timeout_source do |metadata|
+ metadata.present.timeout_source
+ end
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index bbbcf6a97c1..718fb35e62d 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity
expose :id, :reply_id
expose :expanded?, as: :expanded
- expose :notes, using: NoteEntity
+ expose :notes do |discussion, opts|
+ request.note_entity.represent(discussion.notes, opts)
+ end
expose :individual_note?, as: :individual_note
expose :resolvable?, as: :resolvable
@@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
- expose :resolve_with_issue_path do |discussion|
+ expose :resolve_with_issue_path, if: -> (d, _) { d.resolvable? } do |discussion|
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 4ccf0bca476..c964aa9c99b 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note
expose :author, using: NoteUserEntity
- expose :human_access do |note|
- note.project.team.human_max_access(note.author_id)
- end
-
unexpose :note, as: :body
expose :note
@@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note
expose :emoji_awardable?, as: :emoji_awardable
expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
- expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
- if note.for_personal_snippet?
- toggle_award_emoji_snippet_note_path(note.noteable, note)
- else
- toggle_award_emoji_project_note_path(note.project, note.id)
- end
- end
expose :report_abuse_path do |note|
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end
- expose :path do |note|
- if note.for_personal_snippet?
- snippet_note_path(note.noteable, note)
- else
- project_note_path(note.project, note)
- end
- end
-
- expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
- resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
- end
-
- expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
- new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
- end
-
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
- expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
- delete_attachment_project_note_path(note.project, note)
- end
end
diff --git a/app/serializers/note_serializer.rb b/app/serializers/note_serializer.rb
deleted file mode 100644
index 2afe40d7a34..00000000000
--- a/app/serializers/note_serializer.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class NoteSerializer < BaseSerializer
- entity NoteEntity
-end
diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb
new file mode 100644
index 00000000000..e541bfbee8d
--- /dev/null
+++ b/app/serializers/project_note_entity.rb
@@ -0,0 +1,25 @@
+class ProjectNoteEntity < NoteEntity
+ expose :human_access do |note|
+ note.project.team.human_max_access(note.author_id)
+ end
+
+ expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
+ toggle_award_emoji_project_note_path(note.project, note.id)
+ end
+
+ expose :path do |note|
+ project_note_path(note.project, note)
+ end
+
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
+ expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
+ delete_attachment_project_note_path(note.project, note)
+ end
+end
diff --git a/app/serializers/project_note_serializer.rb b/app/serializers/project_note_serializer.rb
new file mode 100644
index 00000000000..763ad0bdb3f
--- /dev/null
+++ b/app/serializers/project_note_serializer.rb
@@ -0,0 +1,3 @@
+class ProjectNoteSerializer < BaseSerializer
+ entity ProjectNoteEntity
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 3e40ecf1c1c..a7c2e21e92b 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- dir = 'ci_favicons'
- dir = File.join(dir, 'dev') if Rails.env.development?
+ dir =
+ if Gitlab::Utils.to_boolean(ENV['CANARY'])
+ File.join('ci_favicons', 'canary')
+ elsif Rails.env.development?
+ File.join('ci_favicons', 'dev')
+ else
+ 'ci_favicons'
+ end
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 15fed7d17c1..3ceab209f3f 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -42,7 +42,10 @@ module Boards
)
end
- attrs[:move_between_ids] = move_between_ids if move_between_ids
+ if move_between_ids
+ attrs[:move_between_ids] = move_between_ids
+ attrs[:board_group_id] = board.group&.id
+ end
attrs
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 6d0dd0a9f99..9269b8d2620 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -2,11 +2,15 @@ module Boards
class ListService < Boards::BaseService
def execute
create_board! if parent.boards.empty?
- parent.boards
+ boards
end
private
+ def boards
+ parent.boards
+ end
+
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index bebc90c7a8d..02f1c709374 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -12,11 +12,15 @@ module Boards
private
def available_labels_for(board)
+ options = { include_ancestor_groups: true }
+
if board.group_board?
- parent.labels
+ options.merge!(group_id: parent.id, only_group_labels: true)
else
- LabelsFinder.new(current_user, project_id: parent.id).execute
+ options[:project_id] = parent.id
end
+
+ LabelsFinder.new(current_user, options).execute
end
def next_position(board)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index ad27f320853..6ce86983287 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -7,6 +7,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
diff --git a/app/services/ci/create_pipeline_stages_service.rb b/app/services/ci/create_pipeline_stages_service.rb
deleted file mode 100644
index f2c175adee6..00000000000
--- a/app/services/ci/create_pipeline_stages_service.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module Ci
- class CreatePipelineStagesService < BaseService
- def execute(pipeline)
- pipeline.stage_seeds.each do |seed|
- seed.user = current_user
-
- seed.create! do |build|
- ##
- # Create the environment before the build starts. This sets its slug and
- # makes it available as an environment variable
- #
- if build.has_environment?
- environment_name = build.expanded_environment_name
- project.environments.find_or_create_by(name: environment_name)
- end
- end
- end
- end
- end
-end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index a9813d774bb..85533a1cbdb 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -16,8 +16,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
- pipeline.trigger_requests.create!(trigger: trigger)
- create_pipeline_variables!(pipeline)
+ pipeline.trigger_requests.build(trigger: trigger)
+ pipeline.variables.build(variables)
end
if pipeline.persisted?
@@ -33,14 +33,10 @@ module Ci
end
end
- def create_pipeline_variables!(pipeline)
- return unless params[:variables]
-
- variables = params[:variables].map do |key, value|
+ def variables
+ params[:variables].to_h.map do |key, value|
{ key: key, value: value }
end
-
- pipeline.variables.create!(variables)
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 02fb48108fb..91ec702fbc6 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService
end
def available_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
end
def handle_quick_actions_on_create(issuable)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 0c5cf2c62ad..fee5bc38f7b 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -23,6 +23,7 @@ module Issues
end
if project.issues_enabled? && issue.close
+ issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index d7aa7e2347e..4161932ad2a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -55,9 +55,10 @@ module Issues
return unless params[:move_between_ids]
after_id, before_id = params.delete(:move_between_ids)
+ board_group_id = params.delete(:board_group_id)
- issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
- issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
+ issue_before = get_issue_if_allowed(before_id, board_group_id)
+ issue_after = get_issue_if_allowed(after_id, board_group_id)
issue.move_between(issue_before, issue_after)
end
@@ -84,8 +85,16 @@ module Issues
private
- def get_issue_if_allowed(project, id)
- issue = project.issues.find(id)
+ def get_issue_if_allowed(id, board_group_id = nil)
+ return unless id
+
+ issue =
+ if board_group_id
+ IssuesFinder.new(current_user, group_id: board_group_id).find_by(id: id)
+ else
+ project.issues.find(id)
+ end
+
issue if can?(current_user, :update_issue, issue)
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 18c40ce8992..1fb1796b56c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,7 +21,7 @@ module MergeRequests
comment_mr_branch_presence_changed
end
- comment_mr_with_commits
+ notify_about_push
mark_mr_as_wip_from_commits
execute_mr_web_hooks
@@ -141,8 +141,8 @@ module MergeRequests
end
end
- # Add comment about pushing new commits to merge requests
- def comment_mr_with_commits
+ # Add comment about pushing new commits to merge requests and send nofitication emails
+ def notify_about_push
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
@@ -155,6 +155,8 @@ module MergeRequests
SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits,
existing_commits, @oldrev)
+
+ notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index ad3dcc5010b..199b8028dbc 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -11,7 +11,7 @@ module Notes
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
- return unless @note.for_project_noteable?
+ return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
@@ -23,9 +23,13 @@ module Notes
end
def execute_note_hooks
+ return unless @note.project
+
note_data = hook_data
- @note.project.execute_hooks(note_data, :note_hooks)
- @note.project.execute_services(note_data, :note_hooks)
+ hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
+
+ @note.project.execute_hooks(note_data, hooks_scope)
+ @note.project.execute_services(note_data, hooks_scope)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d7d2cde1004..f94c76cf3ac 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -113,6 +113,16 @@ class NotificationService
new_resource_email(merge_request, :new_merge_request_email)
end
+ def push_to_merge_request(merge_request, current_user, new_commits: [], existing_commits: [])
+ new_commits = new_commits.map { |c| { short_id: c.short_id, title: c.title } }
+ existing_commits = existing_commits.map { |c| { short_id: c.short_id, title: c.title } }
+ recipients = NotificationRecipientService.build_recipients(merge_request, current_user, action: "push_to")
+
+ recipients.each do |recipient|
+ mailer.send(:push_to_merge_request_email, recipient.user.id, merge_request.id, current_user.id, recipient.reason, new_commits: new_commits, existing_commits: existing_commits).deliver_later
+ end
+ end
+
# When merge request text is updated, we should send an email to:
#
# * newly mentioned project team members with notification level higher than Participating
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index e61ecb696d0..346971138b1 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -21,7 +21,8 @@ module Projects
end
def labels(target = nil)
- labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
+ labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
+ .execute.select([:color, :title])
return labels unless target&.respond_to?(:labels)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7fa1387084c..633e2c8236c 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -90,9 +90,6 @@ module Projects
unless @project.gitlab_project_import?
@project.write_repository_config
@project.create_wiki unless skip_wiki?
- create_services_from_active_templates(@project)
-
- @project.create_labels
end
event_service.create_project(@project, current_user)
@@ -121,21 +118,29 @@ module Projects
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
- if @project.save && !@project.import?
- raise 'Failed to create repository' unless @project.create_repository
+ if @project.save
+ unless @project.gitlab_project_import?
+ create_services_from_active_templates(@project)
+ @project.create_labels
+ end
+
+ unless @project.import?
+ raise 'Failed to create repository' unless @project.create_repository
+ end
end
end
end
def fail(error:)
message = "Unable to save project. Error: #{error}"
- message << "Project ID: #{@project.id}" if @project && @project.id
+ log_message = message.dup
- Rails.logger.error(message)
+ log_message << " Project ID: #{@project.id}" if @project&.id
+ Rails.logger.error(log_message)
- if @project && @project.import?
+ if @project
@project.errors.add(:base, message)
- @project.mark_import_as_failed(message)
+ @project.mark_import_as_failed(message) if @project.import?
end
@project
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
index a68ecb4abe1..fb4afb85588 100644
--- a/app/services/projects/gitlab_projects_import_service.rb
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -5,8 +5,8 @@ module Projects
class GitlabProjectsImportService
attr_reader :current_user, :params
- def initialize(user, params)
- @current_user, @params = user, params.dup
+ def initialize(user, import_params, override_params = nil)
+ @current_user, @params, @override_params = user, import_params.dup, override_params
end
def execute
@@ -17,6 +17,7 @@ module Projects
params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path
+ params[:import_data] = { data: { override_params: @override_params } } if @override_params
::Projects::CreateService.new(current_user, params).execute
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d16aa3de639..7bf0b90b491 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -1,22 +1,36 @@
module Projects
module ImportExport
class ExportService < BaseService
- def execute(_options = {})
+ def execute(after_export_strategy = nil, options = {})
@shared = project.import_export_shared
- save_all
+
+ save_all!
+ execute_after_export_action(after_export_strategy)
end
private
- def save_all
- if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ def execute_after_export_action(after_export_strategy)
+ return unless after_export_strategy
+
+ unless after_export_strategy.execute(current_user, project)
+ cleanup_and_notify_error
+ end
+ end
+
+ def save_all!
+ if save_services
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success
else
- cleanup_and_notify
+ cleanup_and_notify_error!
end
end
+ def save_services
+ [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
+ end
+
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
@@ -41,19 +55,26 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
- def cleanup_and_notify
+ def lfs_saver
+ Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
+ end
+
+ def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path)
notify_error
+ end
+
+ def cleanup_and_notify_error!
+ cleanup_and_notify_error
+
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end
def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
-
- notification_service.project_exported(@project, @current_user)
end
def notify_error
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index a34024f4f80..bdd9598f85a 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,11 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
- raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ begin
+ Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Error, "Blocked import URL: #{e.message}"
+ end
end
# We should skip the repository for a GitHub import or GitLab project import,
@@ -57,7 +61,7 @@ module Projects
project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else
- gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
+ gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url)
end
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as:
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 00fdd047208..7e228d1833d 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -31,15 +31,17 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
- raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvaildStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
- rescue InvaildStateError, FailedToExtractError => e
- register_failure
+ rescue InvaildStateError => e
error(e.message)
+ rescue => e
+ error(e.message, false)
+ raise e
end
private
@@ -50,12 +52,13 @@ module Projects
super
end
- def error(message, http_status = nil)
+ def error(message, allow_delete_artifact = true)
+ register_failure
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
- delete_artifact!
+ delete_artifact! if allow_delete_artifact
super
end
@@ -76,26 +79,28 @@ module Projects
elsif artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
- raise FailedToExtractError, 'unsupported artifacts format'
+ raise InvaildStateError, 'unsupported artifacts format'
end
end
def extract_tar_archive!(temp_path)
- results = Open3.pipeline(%W(gunzip -c #{artifacts}),
- %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
- %W(tar -x -C #{temp_path} #{SITE_PATH}),
- err: '/dev/null')
- raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
+ build.artifacts_file.use_file do |artifacts_path|
+ results = Open3.pipeline(%W(gunzip -c #{artifacts_path}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
end
def extract_zip_archive!(temp_path)
- raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
+ raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
- raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
+ raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
@@ -103,8 +108,10 @@ module Projects
# -n never overwrite existing files
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*')
- unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path}))
- raise FailedToExtractError, 'pages failed to extract'
+ build.artifacts_file.use_file do |artifacts_path|
+ unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
+ raise FailedToExtractError, 'pages failed to extract'
+ end
end
end
@@ -174,6 +181,9 @@ module Projects
def latest_sha
project.commit(build.ref).try(:sha).to_s
+ ensure
+ # Close any file descriptors that were opened and free libgit2 buffers
+ project.cleanup
end
def sha
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 6212fd69077..9d947f73af1 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -1,11 +1,20 @@
module ProtectedBranches
class CreateService < BaseService
- attr_reader :protected_branch
-
def execute(skip_authorization: false)
- raise Gitlab::Access::AccessDeniedError unless skip_authorization || can?(current_user, :admin_project, project)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?
+
+ protected_branch.save
+ protected_branch
+ end
+
+ def authorized?
+ can?(current_user, :create_protected_branch, protected_branch)
+ end
+
+ private
- project.protected_branches.create(params)
+ def protected_branch
+ @protected_branch ||= project.protected_branches.new(params)
end
end
end
diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb
new file mode 100644
index 00000000000..8172c896e76
--- /dev/null
+++ b/app/services/protected_branches/destroy_service.rb
@@ -0,0 +1,9 @@
+module ProtectedBranches
+ class DestroyService < BaseService
+ def execute(protected_branch)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch)
+
+ protected_branch.destroy
+ end
+ end
+end
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index 4b3337a5c9d..95e46645374 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -1,7 +1,7 @@
module ProtectedBranches
class UpdateService < BaseService
def execute(protected_branch)
- raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch)
protected_branch.update(params)
protected_branch
diff --git a/app/services/protected_tags/destroy_service.rb b/app/services/protected_tags/destroy_service.rb
new file mode 100644
index 00000000000..c868d7ad8e6
--- /dev/null
+++ b/app/services/protected_tags/destroy_service.rb
@@ -0,0 +1,7 @@
+module ProtectedTags
+ class DestroyService < BaseService
+ def execute(protected_tag)
+ protected_tag.destroy
+ end
+ end
+end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index cba49faac31..6cc51b6ee1b 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -200,7 +200,7 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+ available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
@@ -562,7 +562,7 @@ module QuickActions
def find_labels(labels_param)
extract_references(labels_param, :label) |
- LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
+ LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
end
def find_label_references(labels_param)
@@ -593,6 +593,7 @@ module QuickActions
def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user)
+
ext.analyze(arg, author: current_user)
ext.references(type)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 2253d638e93..00bf5434b7f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -429,7 +429,7 @@ module SystemNoteService
def cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
- gfm_reference = mentioner.gfm_reference(noteable.project)
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
@@ -582,7 +582,7 @@ module SystemNoteService
text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}"
notes.where('(note LIKE ? OR note LIKE ?)', text, text.capitalize)
else
- gfm_reference = mentioner.gfm_reference(noteable.project)
+ gfm_reference = mentioner.gfm_reference(noteable.project || noteable.group)
text = cross_reference_note_content(gfm_reference)
notes.where(note: [text, text.capitalize])
end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index 86166047302..13cb53dee01 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -34,7 +34,8 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
- domain.update!(verified_at: Time.now, enabled_until: reverify)
+ domain.assign_attributes(verified_at: Time.now, enabled_until: reverify)
+ domain.save!(validate: false)
if was_disabled
notify(:enabled)
@@ -47,7 +48,9 @@ class VerifyPagesDomainService < BaseService
def unverify_domain!
if domain.verified?
- domain.update!(verified_at: nil)
+ domain.assign_attributes(verified_at: nil)
+ domain.save!(validate: false)
+
notify(:verification_failed)
end
@@ -55,7 +58,8 @@ class VerifyPagesDomainService < BaseService
end
def disable_domain!
- domain.update!(verified_at: nil, enabled_until: nil)
+ domain.assign_attributes(verified_at: nil, enabled_until: nil)
+ domain.save!(validate: false)
notify(:disabled)
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 4930fb2fca7..cd819dc9bff 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,8 +1,8 @@
class AttachmentUploader < GitlabUploader
- include UploaderHelper
include RecordsUploads::Concern
-
- storage :file
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
+ include UploaderHelper
private
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 5c8e1cea62e..5848e6c6994 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,18 +1,18 @@
class AvatarUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
-
- storage :file
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
def exists?
model.avatar.file && model.avatar.file.present?
end
- def move_to_cache
+ def move_to_store
false
end
- def move_to_store
+ def move_to_cache
false
end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 8f56f09c9f7..bd7736ad74e 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -10,7 +10,11 @@ class FileMover
def execute
move
- uploader.record_upload if update_markdown
+
+ if update_markdown
+ uploader.record_upload
+ uploader.schedule_background_upload
+ end
end
private
@@ -24,11 +28,8 @@ class FileMover
updated_text = model.read_attribute(update_field)
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
model.update_attribute(update_field, updated_text)
-
- true
rescue
revert
-
false
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index bde1161dfa8..133fdf6684d 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -9,14 +9,18 @@
class FileUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
- storage :file
-
after :remove, :prune_store_dir
+ # FileUploader do not run in a model transaction, so we can simply
+ # enqueue a job after the :store hook.
+ after :store, :schedule_background_upload
+
def self.root
File.join(options.storage_path, 'uploads')
end
@@ -28,8 +32,11 @@ class FileUploader < GitlabUploader
)
end
- def self.base_dir(model)
- model_path_segment(model)
+ def self.base_dir(model, store = Store::LOCAL)
+ decorated_model = model
+ decorated_model = Storage::HashedProject.new(model) if store == Store::REMOTE
+
+ model_path_segment(decorated_model)
end
# used in migrations and import/exports
@@ -47,21 +54,24 @@ class FileUploader < GitlabUploader
#
# Returns a String without a trailing slash
def self.model_path_segment(model)
- if model.hashed_storage?(:attachments)
- model.disk_path
+ case model
+ when Storage::HashedProject then model.disk_path
else
- model.full_path
+ model.hashed_storage?(:attachments) ? model.disk_path : model.full_path
end
end
- def self.upload_path(secret, identifier)
- File.join(secret, identifier)
- end
-
def self.generate_secret
SecureRandom.hex
end
+ def upload_paths(filename)
+ [
+ File.join(secret, filename),
+ File.join(base_dir(Store::REMOTE), secret, filename)
+ ]
+ end
+
attr_accessor :model
def initialize(model, mounted_as = nil, **uploader_context)
@@ -71,8 +81,10 @@ class FileUploader < GitlabUploader
apply_context!(uploader_context)
end
- def base_dir
- self.class.base_dir(@model)
+ # enforce the usage of Hashed storage when storing to
+ # remote store as the FileMover doesn't support OS
+ def base_dir(store = nil)
+ self.class.base_dir(@model, store || object_store)
end
# we don't need to know the actual path, an uploader instance should be
@@ -82,15 +94,19 @@ class FileUploader < GitlabUploader
end
def upload_path
- self.class.upload_path(dynamic_segment, identifier)
- end
-
- def model_path_segment
- self.class.model_path_segment(@model)
+ if file_storage?
+ # Legacy path relative to project.full_path
+ File.join(dynamic_segment, identifier)
+ else
+ File.join(store_dir, identifier)
+ end
end
- def store_dir
- File.join(base_dir, dynamic_segment)
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(base_dir(ObjectStorage::Store::REMOTE), dynamic_segment)
+ }
end
def markdown_link
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 010100f2da1..f12f0466a1d 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -37,12 +37,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
cache_storage.is_a?(CarrierWave::Storage::File)
end
- # Reduce disk IO
def move_to_cache
file_storage?
end
- # Reduce disk IO
def move_to_store
file_storage?
end
@@ -51,10 +49,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present?
end
- def store_dir
- File.join(base_dir, dynamic_segment)
- end
-
def cache_dir
File.join(root, base_dir, 'tmp/cache')
end
@@ -76,6 +70,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Designed to be overridden by child uploaders that have a dynamic path
# segment -- that is, a path that changes based on mutable attributes of its
# associated model
+ #
+ # For example, `FileUploader` builds the storage path based on the associated
+ # project model's `path_with_namespace` value, which can change when the
+ # project or its containing namespace is moved or renamed.
def dynamic_segment
raise(NotImplementedError)
end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index ad5385f45a4..ef0f8acefd6 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -1,5 +1,6 @@
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
+ include ObjectStorage::Concern
storage_options Gitlab.config.artifacts
@@ -14,9 +15,11 @@ class JobArtifactUploader < GitlabUploader
end
def open
- raise 'Only File System is supported' unless file_storage?
-
- File.open(path, "rb") if path
+ if file_storage?
+ File.open(path, "rb") if path
+ else
+ ::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
+ end
end
private
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index 28c458d3ff1..b726b053493 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,5 +1,6 @@
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
+ include ObjectStorage::Concern
storage_options Gitlab.config.artifacts
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index e04c97ce179..eb521a22ebc 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,10 +1,6 @@
class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath
-
- # LfsObject are in `tmp/upload` instead of `tmp/uploads`
- def self.workhorse_upload_path
- File.join(root, 'tmp/upload')
- end
+ include ObjectStorage::Concern
storage_options Gitlab.config.lfs
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 993e85fbc13..1085ecb1700 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -4,7 +4,7 @@ class NamespaceFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model)
+ def self.base_dir(model, _store = nil)
File.join(options.base_dir, 'namespace', model_path_segment(model))
end
@@ -14,6 +14,13 @@ class NamespaceFileUploader < FileUploader
# Re-Override
def store_dir
- File.join(base_dir, dynamic_segment)
+ store_dirs[object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
+ }
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
new file mode 100644
index 00000000000..3f59ee39299
--- /dev/null
+++ b/app/uploaders/object_storage.rb
@@ -0,0 +1,442 @@
+require 'fog/aws'
+require 'carrierwave/storage/fog'
+
+#
+# This concern should add object storage support
+# to the GitlabUploader class
+#
+module ObjectStorage
+ RemoteStoreError = Class.new(StandardError)
+ UnknownStoreError = Class.new(StandardError)
+ ObjectStorageUnavailable = Class.new(StandardError)
+
+ DIRECT_UPLOAD_TIMEOUT = 4.hours
+ TMP_UPLOAD_PATH = 'tmp/upload'.freeze
+
+ module Store
+ LOCAL = 1
+ REMOTE = 2
+ end
+
+ module Extension
+ # this extension is the glue between the ObjectStorage::Concern and RecordsUploads::Concern
+ module RecordsUploads
+ extend ActiveSupport::Concern
+
+ def prepended(base)
+ raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern
+
+ base.include(RecordsUploads::Concern)
+ end
+
+ def retrieve_from_store!(identifier)
+ paths = store_dirs.map { |store, path| File.join(path, identifier) }
+
+ unless current_upload_satisfies?(paths, model)
+ # the upload we already have isn't right, find the correct one
+ self.upload = uploads.find_by(model: model, path: paths)
+ end
+
+ super
+ end
+
+ def build_upload
+ super.tap do |upload|
+ upload.store = object_store
+ end
+ end
+
+ def upload=(upload)
+ return unless upload
+
+ self.object_store = upload.store
+ super
+ end
+
+ def schedule_background_upload(*args)
+ return unless schedule_background_upload?
+ return unless upload
+
+ ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
+ upload.class.to_s,
+ mounted_as,
+ upload.id)
+ end
+
+ private
+
+ def current_upload_satisfies?(paths, model)
+ return false unless upload
+ return false unless model
+
+ paths.include?(upload.path) &&
+ upload.model_id == model.id &&
+ upload.model_type == model.class.base_class.sti_name
+ end
+ end
+ end
+
+ # Add support for automatic background uploading after the file is stored.
+ #
+ module BackgroundMove
+ extend ActiveSupport::Concern
+
+ def background_upload(mount_points = [])
+ return unless mount_points.any?
+
+ run_after_commit do
+ mount_points.each { |mount| send(mount).schedule_background_upload } # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def changed_mounts
+ self.class.uploaders.select do |mount, uploader_class|
+ mounted_as = uploader_class.serialization_column(self.class, mount)
+ uploader = send(:"#{mounted_as}") # rubocop:disable GitlabSecurity/PublicSend
+
+ next unless uploader
+ next unless uploader.exists?
+ next unless send(:"#{mounted_as}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+
+ mount
+ end.keys
+ end
+
+ included do
+ after_save on: [:create, :update] do
+ background_upload(changed_mounts)
+ end
+ end
+ end
+
+ module Concern
+ extend ActiveSupport::Concern
+
+ included do |base|
+ base.include(ObjectStorage)
+
+ after :migrate, :delete_migrated_file
+ end
+
+ class_methods do
+ def object_store_options
+ options.object_store
+ end
+
+ def object_store_enabled?
+ object_store_options.enabled
+ end
+
+ def direct_upload_enabled?
+ object_store_options&.direct_upload
+ end
+
+ def background_upload_enabled?
+ object_store_options.background_upload
+ end
+
+ def proxy_download_enabled?
+ object_store_options.proxy_download
+ end
+
+ def direct_download_enabled?
+ !proxy_download_enabled?
+ end
+
+ def object_store_credentials
+ object_store_options.connection.to_hash.deep_symbolize_keys
+ end
+
+ def remote_store_path
+ object_store_options.remote_directory
+ end
+
+ def serialization_column(model_class, mount_point)
+ model_class.uploader_options.dig(mount_point, :mount_on) || mount_point
+ end
+
+ def workhorse_authorize
+ if options = workhorse_remote_upload_options
+ { RemoteObject: options }
+ else
+ { TempPath: workhorse_local_upload_path }
+ end
+ end
+
+ def workhorse_local_upload_path
+ File.join(self.root, TMP_UPLOAD_PATH)
+ end
+
+ def workhorse_remote_upload_options
+ return unless self.object_store_enabled?
+ return unless self.direct_upload_enabled?
+
+ id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
+ upload_path = File.join(TMP_UPLOAD_PATH, id)
+ connection = ::Fog::Storage.new(self.object_store_credentials)
+ expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT
+ options = { 'Content-Type' => 'application/octet-stream' }
+
+ {
+ ID: id,
+ GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at),
+ DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at),
+ StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
+ }
+ end
+
+ def default_object_store
+ if self.object_store_enabled? && self.direct_upload_enabled?
+ Store::REMOTE
+ else
+ Store::LOCAL
+ end
+ end
+ end
+
+ # allow to configure and overwrite the filename
+ def filename
+ @filename || super || file&.filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def filename=(filename)
+ @filename = filename # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def file_storage?
+ storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def file_cache_storage?
+ cache_storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def object_store
+ @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def object_store=(value)
+ @object_store = value || self.class.default_object_store
+ @storage = storage_for(object_store)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ # Return true if the current file is part or the model (i.e. is mounted in the model)
+ #
+ def persist_object_store?
+ model.respond_to?(:"#{store_serialization_column}=")
+ end
+
+ # Save the current @object_store to the model <mounted_as>_store column
+ def persist_object_store!
+ return unless persist_object_store?
+
+ updated = model.update_column(store_serialization_column, object_store)
+ raise 'Failed to update object store' unless updated
+ end
+
+ def use_file(&blk)
+ with_exclusive_lease do
+ unsafe_use_file(&blk)
+ end
+ end
+
+ #
+ # Move the file to another store
+ #
+ # new_store: Enum (Store::LOCAL, Store::REMOTE)
+ #
+ def migrate!(new_store)
+ with_exclusive_lease do
+ unsafe_migrate!(new_store)
+ end
+ end
+
+ def schedule_background_upload(*args)
+ return unless schedule_background_upload?
+
+ ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
+ model.class.name,
+ mounted_as,
+ model.id)
+ end
+
+ def fog_directory
+ self.class.remote_store_path
+ end
+
+ def fog_credentials
+ self.class.object_store_credentials
+ end
+
+ def fog_public
+ false
+ end
+
+ def delete_migrated_file(migrated_file)
+ migrated_file.delete if exists?
+ end
+
+ def exists?
+ file.present?
+ end
+
+ def store_dir(store = nil)
+ store_dirs[store || object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(dynamic_segment)
+ }
+ end
+
+ def store_workhorse_file!(params, identifier)
+ filename = params["#{identifier}.name"]
+
+ if remote_object_id = params["#{identifier}.remote_id"]
+ store_remote_file!(remote_object_id, filename)
+ elsif local_path = params["#{identifier}.path"]
+ store_local_file!(local_path, filename)
+ else
+ raise RemoteStoreError, 'Bad file'
+ end
+ end
+
+ private
+
+ def schedule_background_upload?
+ self.class.object_store_enabled? &&
+ self.class.background_upload_enabled? &&
+ self.file_storage?
+ end
+
+ def store_remote_file!(remote_object_id, filename)
+ raise RemoteStoreError, 'Missing filename' unless filename
+
+ file_path = File.join(TMP_UPLOAD_PATH, remote_object_id)
+ file_path = Pathname.new(file_path).cleanpath.to_s
+ raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(TMP_UPLOAD_PATH + '/')
+
+ self.object_store = Store::REMOTE
+
+ # TODO:
+ # This should be changed to make use of `tmp/cache` mechanism
+ # instead of using custom upload directory,
+ # using tmp/cache makes this implementation way easier than it is today
+ CarrierWave::Storage::Fog::File.new(self, storage, file_path).tap do |file|
+ raise RemoteStoreError, 'Missing file' unless file.exists?
+
+ self.filename = filename
+ self.file = storage.store!(file)
+ end
+ end
+
+ def store_local_file!(local_path, filename)
+ raise RemoteStoreError, 'Missing filename' unless filename
+
+ root_path = File.realpath(self.class.workhorse_local_upload_path)
+ file_path = File.realpath(local_path)
+ raise RemoteStoreError, 'Bad file path' unless file_path.start_with?(root_path)
+
+ self.object_store = Store::LOCAL
+ self.store!(UploadedFile.new(file_path, filename))
+ end
+
+ # this is a hack around CarrierWave. The #migrate method needs to be
+ # able to force the current file to the migrated file upon success.
+ def file=(file)
+ @file = file # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def serialization_column
+ self.class.serialization_column(model.class, mounted_as)
+ end
+
+ # Returns the column where the 'store' is saved
+ # defaults to 'store'
+ def store_serialization_column
+ [serialization_column, 'store'].compact.join('_').to_sym
+ end
+
+ def storage
+ @storage ||= storage_for(object_store)
+ end
+
+ def storage_for(store)
+ case store
+ when Store::REMOTE
+ raise 'Object Storage is not enabled' unless self.class.object_store_enabled?
+
+ CarrierWave::Storage::Fog.new(self)
+ when Store::LOCAL
+ CarrierWave::Storage::File.new(self)
+ else
+ raise UnknownStoreError
+ end
+ end
+
+ def exclusive_lease_key
+ "object_storage_migrate:#{model.class}:#{model.id}"
+ end
+
+ def with_exclusive_lease
+ uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ raise 'exclusive lease already taken' unless uuid
+
+ yield uuid
+ ensure
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ end
+
+ #
+ # Move the file to another store
+ #
+ # new_store: Enum (Store::LOCAL, Store::REMOTE)
+ #
+ def unsafe_migrate!(new_store)
+ return unless object_store != new_store
+ return unless file
+
+ new_file = nil
+ file_to_delete = file
+ from_object_store = object_store
+ self.object_store = new_store # changes the storage and file
+
+ cache_stored_file! if file_storage?
+
+ with_callbacks(:migrate, file_to_delete) do
+ with_callbacks(:store, file_to_delete) do # for #store_versions!
+ new_file = storage.store!(file)
+ persist_object_store!
+ self.file = new_file
+ end
+ end
+
+ file
+ rescue => e
+ # in case of failure delete new file
+ new_file.delete unless new_file.nil?
+ # revert back to the old file
+ self.object_store = from_object_store
+ self.file = file_to_delete
+ raise e
+ end
+ end
+
+ def unsafe_use_file
+ if file_storage?
+ return yield path
+ end
+
+ begin
+ cache_stored_file!
+ yield cache_path
+ ensure
+ FileUtils.rm_f(cache_path)
+ cache_storage.delete_dir!(cache_path(nil))
+ end
+ end
+end
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index f2ad0badd53..e3898b07730 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model)
+ def self.base_dir(model, _store = nil)
File.join(options.base_dir, model_path_segment(model))
end
@@ -14,6 +14,12 @@ class PersonalFileUploader < FileUploader
File.join(model.class.to_s.underscore, model.id.to_s)
end
+ def object_store
+ return Store::LOCAL unless model
+
+ super
+ end
+
# model_path_segment does not require a model to be passed, so we can always
# generate a path, even when there's no model.
def model_valid?
@@ -22,7 +28,14 @@ class PersonalFileUploader < FileUploader
# Revert-Override
def store_dir
- File.join(base_dir, dynamic_segment)
+ store_dirs[object_store]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => File.join(self.class.model_path_segment(model), dynamic_segment)
+ }
end
private
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 458928bc067..89c74a78835 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -24,8 +24,7 @@ module RecordsUploads
uploads.where(path: upload_path).delete_all
upload.destroy! if upload
- self.upload = build_upload
- upload.save!
+ self.upload = build_upload.tap(&:save!)
end
end
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
new file mode 100644
index 00000000000..17df756183a
--- /dev/null
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -0,0 +1,9 @@
+class CertificateFingerprintValidator < ActiveModel::EachValidator
+ FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value.try(:match, FINGERPRINT_PATTERN)
+ record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
+ end
+ end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 3ec1594e202..612d3c71913 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,8 +4,8 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
- record.errors.add(attribute, "imports are not allowed from that URL")
- end
+ Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ record.errors.add(attribute, "is blocked: #{e.message}")
end
end
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
new file mode 100644
index 00000000000..7e2e735e0cf
--- /dev/null
+++ b/app/validators/top_level_group_validator.rb
@@ -0,0 +1,7 @@
+class TopLevelGroupValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value&.subgroup?
+ record.errors.add(attribute, "must be a top level Group")
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
new file mode 100644
index 00000000000..bb3fa26a33e
--- /dev/null
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -0,0 +1,12 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
new file mode 100644
index 00000000000..dd86c9ed2eb
--- /dev/null
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -0,0 +1,39 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :gravatar_enabled do
+ = f.check_box :gravatar_enabled
+ Gravatar enabled
+ .form-group
+ = f.label :default_projects_limit, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :default_projects_limit, class: 'form-control'
+ .form-group
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_attachment_size, class: 'form-control'
+ .form-group
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :session_expire_delay, class: 'form-control'
+ %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
+ .form-group
+ = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :user_oauth_applications do
+ = f.check_box :user_oauth_applications
+ Allow users to register any application to use GitLab as an OAuth provider
+ .form-group
+ = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :user_default_external do
+ = f.check_box :user_default_external
+ Newly registered users will by default be external
+
+ = f.submit 'Save changes', class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
new file mode 100644
index 00000000000..8198a822a10
--- /dev/null
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -0,0 +1,30 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
new file mode 100644
index 00000000000..b4d2a789df0
--- /dev/null
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -0,0 +1,47 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :auto_devops_enabled do
+ = f.check_box :auto_devops_enabled
+ Enabled Auto DevOps (Beta) for projects by default
+ .help-block
+ It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
+ .form-group
+ = f.label :auto_devops_domain, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
+ .help-block
+ = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :shared_runners_enabled do
+ = f.check_box :shared_runners_enabled
+ Enable shared runners for new projects
+ .form-group
+ = f.label :shared_runners_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :shared_runners_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+ .form-group
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_artifacts_size, class: 'form-control'
+ .help-block
+ Set the maximum file size for each job's artifacts
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ .form-group
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :default_artifacts_expire_in, class: 'form-control'
+ .help-block
+ Set the default expiration time for each job's artifacts.
+ 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
new file mode 100644
index 00000000000..6c89f1c4e98
--- /dev/null
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -0,0 +1,26 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :html_emails_enabled do
+ = f.check_box :html_emails_enabled
+ Enable HTML emails
+ .help-block
+ By default GitLab sends emails in HTML and plain text formats so mail
+ clients can choose what format to use. Disable this option if you only
+ want to send emails in plain text format.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
deleted file mode 100644
index 54b39df8cf3..00000000000
--- a/app/views/admin/application_settings/_form.html.haml
+++ /dev/null
@@ -1,873 +0,0 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %fieldset
- %legend Visibility and Access Controls
- .form-group
- = f.label :default_branch_protection, class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- .form-group.visibility-level-setting
- = f.label :default_project_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
- .form-group.visibility-level-setting
- = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.visibility-level-setting
- = f.label :default_group_visibility, class: 'control-label col-sm-2'
- .col-sm-10
- = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
- .form-group
- = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
- .col-sm-10
- - checkbox_name = 'application_setting[restricted_visibility_levels][]'
- = hidden_field_tag(checkbox_name)
- - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- .checkbox
- = level
- %span.help-block#restricted-visibility-help
- Selected levels cannot be used by non-admin users for projects or snippets.
- If the public level is restricted, user profiles are only visible to logged in users.
- .form-group
- = f.label :import_sources, class: 'control-label col-sm-2'
- .col-sm-10
- - import_sources_checkboxes('import-sources-help').each do |source|
- .checkbox= source
- %span.help-block#import-sources-help
- Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
- = link_to "(?)", help_page_path("integration/github")
- , Bitbucket
- = link_to "(?)", help_page_path("integration/bitbucket")
- and GitLab.com
- = link_to "(?)", help_page_path("integration/gitlab")
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :project_export_enabled do
- = f.check_box :project_export_enabled
- Project export enabled
-
- .form-group
- %label.control-label.col-sm-2 Enabled Git access protocols
- .col-sm-10
- = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.help-block#clone-protocol-help
- Allow only the selected protocols to be used for Git access.
-
- - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- - field_name = :"#{type}_key_restriction"
- .form-group
- = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
- .col-sm-10
- = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
-
- %fieldset
- %legend Account and Limit Settings
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :gravatar_enabled do
- = f.check_box :gravatar_enabled
- Gravatar enabled
- .form-group
- = f.label :default_projects_limit, class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :default_projects_limit, class: 'form-control'
- .form-group
- = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_attachment_size, class: 'form-control'
- .form-group
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :session_expire_delay, class: 'form-control'
- %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
- .form-group
- = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_oauth_applications do
- = f.check_box :user_oauth_applications
- Allow users to register any application to use GitLab as an OAuth provider
- .form-group
- = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :user_default_external do
- = f.check_box :user_default_external
- Newly registered users will by default be external
-
- %fieldset
- %legend Sign-up Restrictions
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :signup_enabled do
- = f.check_box :signup_enabled
- Sign-up enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :send_user_confirmation_email do
- = f.check_box :send_user_confirmation_email
- Send confirmation email on sign-up
- .form-group
- = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group
- = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :domain_blacklist_enabled do
- = f.check_box :domain_blacklist_enabled
- Enable domain blacklist for sign ups
- .form-group
- .col-sm-offset-2.col-sm-10
- .radio
- = label_tag :blacklist_type_file do
- = radio_button_tag :blacklist_type, :file
- .option-title
- Upload blacklist file
- .radio
- = label_tag :blacklist_type_raw do
- = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
- .option-title
- Enter blacklist manually
- .form-group.blacklist-file
- = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
- .col-sm-10
- = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
- .form-group.blacklist-raw
- = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
-
- .form-group
- = f.label :after_sign_up_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
-
- %fieldset
- %legend Sign-in Restrictions
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :password_authentication_enabled_for_web do
- = f.check_box :password_authentication_enabled_for_web
- Password authentication enabled for web interface
- .help-block
- When disabled, an external authentication provider must be used.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :password_authentication_enabled_for_git do
- = f.check_box :password_authentication_enabled_for_git
- Password authentication enabled for Git over HTTP(S)
- .help-block
- When disabled, a Personal Access Token
- - if Gitlab::Auth::LDAP::Config.enabled?
- or LDAP password
- must be used to authenticate.
- - if omniauth_enabled? && button_based_providers.any?
- .form-group
- = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
- .col-sm-10
- .btn-group{ data: { toggle: 'buttons' } }
- - oauth_providers_checkboxes.each do |source|
- = source
- .form-group
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- Require all users to setup Two-factor authentication
- .form-group
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
- .form-group
- = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.help-block#home_help_block We will redirect non-logged in users to this page
- .form-group
- = f.label :after_sign_out_path, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
- .form-group
- = f.label :sign_in_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :sign_in_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
-
- %fieldset
- %legend Help Page
- .form-group
- = f.label :help_page_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :help_page_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :help_page_hide_commercial_content do
- = f.check_box :help_page_hide_commercial_content
- Hide marketing-related entries from help
- .form-group
- = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.help-block#support_help_block Alternate support URL for help page
-
- %fieldset
- %legend Pages
- .form-group
- = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_pages_size, class: 'form-control'
- .help-block 0 for unlimited
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :pages_domain_verification_enabled do
- = f.check_box :pages_domain_verification_enabled
- Require users to prove ownership of custom domains
- .help-block
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
- = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
-
- %fieldset
- %legend Continuous Integration and Deployment
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :auto_devops_enabled do
- = f.check_box :auto_devops_enabled
- Enabled Auto DevOps (Beta) for projects by default
- .help-block
- It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
- .form-group
- = f.label :auto_devops_domain, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
- .help-block
- = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :shared_runners_enabled do
- = f.check_box :shared_runners_enabled
- Enable shared runners for new projects
- .form-group
- = f.label :shared_runners_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :shared_runners_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
- = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :max_artifacts_size, class: 'form-control'
- .help-block
- Set the maximum file size for each job's artifacts
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
- .form-group
- = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :default_artifacts_expire_in, class: 'form-control'
- .help-block
- Set the default expiration time for each job's artifacts.
- 0 for unlimited.
- = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
-
- - if Gitlab.config.registry.enabled
- %fieldset
- %legend Container Registry
- .form-group
- = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :container_registry_token_expire_delay, class: 'form-control'
-
- %fieldset
- %legend Metrics - Influx
- %p
- Setup InfluxDB to measure a wide variety of statistics like the time spent
- in running SQL queries. These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :metrics_enabled do
- = f.check_box :metrics_enabled
- Enable InfluxDB Metrics
- .form-group
- = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
- .form-group
- = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
- .help-block
- The UDP port to use for connecting to InfluxDB. InfluxDB requires that
- your server configuration specifies a database to store data in when
- sending messages to this port, without it metrics data will not be
- saved.
- .form-group
- = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_pool_size, class: 'form-control'
- .help-block
- The amount of InfluxDB connections to open. Connections are opened
- lazily. Users using multi-threaded application servers should ensure
- enough connections are available (at minimum the amount of application
- server threads).
- .form-group
- = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_timeout, class: 'form-control'
- .help-block
- The amount of seconds after which an InfluxDB connection will time
- out.
- .form-group
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_method_call_threshold, class: 'form-control'
- .help-block
- A method call is only tracked when it takes longer to complete than
- the given amount of milliseconds.
- .form-group
- = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_sample_interval, class: 'form-control'
- .help-block
- The sampling interval in seconds. Sampled data includes memory usage,
- retained Ruby objects, file descriptors and so on.
- .form-group
- = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :metrics_packet_size, class: 'form-control'
- .help-block
- The amount of points to store in a single UDP packet. More points
- results in fewer but larger UDP packets being sent.
-
- %fieldset
- %legend Metrics - Prometheus
- %p
- Enable a Prometheus metrics endpoint at
- %code= metrics_path
- to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available
- = link_to 'here', admin_health_check_path
- \. This setting requires a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :prometheus_metrics_enabled do
- = f.check_box :prometheus_metrics_enabled
- Enable Prometheus Metrics
- - unless Gitlab::Metrics.metrics_folder_present?
- .help-block
- %strong.cred WARNING:
- Environment variable
- %code prometheus_multiproc_dir
- does not exist or is not pointing to a valid directory.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
-
- %fieldset
- %legend Profiling - Performance Bar
- %p
- Enable the Performance Bar for a given group.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :performance_bar_enabled do
- = f.check_box :performance_bar_enabled
- Enable the Performance Bar
- .form-group
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
-
- %fieldset
- %legend Background Jobs
- %p
- These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sidekiq_throttling_enabled do
- = f.check_box :sidekiq_throttling_enabled
- Enable Sidekiq Job Throttling
- .help-block
- Limit the amount of resources slow running jobs are assigned.
- .form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .help-block
- Choose which queues you wish to throttle.
- .form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .help-block
- The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
-
- %fieldset
- %legend Spam and Anti-bot Protection
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :recaptcha_enabled do
- = f.check_box :recaptcha_enabled
- Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
-
- .form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_site_key, class: 'form-control'
- .help-block
- Generate site and private keys at
- %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
-
- .form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :recaptcha_private_key, class: 'form-control'
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :akismet_enabled do
- = f.check_box :akismet_enabled
- Enable Akismet
- %span.help-block#akismet_help_block Helps prevent bots from creating issues
-
- .form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :akismet_api_key, class: 'form-control'
- .help-block
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :unique_ips_limit_enabled do
- = f.check_box :unique_ips_limit_enabled
- Limit sign in from multiple ips
- %span.help-block#unique_ip_help_block
- Helps prevent malicious users hide their activity
-
- .form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .help-block
- Maximum number of unique IPs per user
-
- .form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .help-block
- How many seconds an IP will be counted towards the limit
-
- %fieldset
- %legend Abuse reports
- .form-group
- = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .help-block
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
-
- %fieldset
- %legend Error Reporting and Logging
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :sentry_enabled do
- = f.check_box :sentry_enabled
- Enable Sentry
- .help-block
- %p This setting requires a restart to take effect.
- Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
-
- .form-group
- = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :sentry_dsn, class: 'form-control'
-
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :clientside_sentry_enabled do
- = f.check_box :clientside_sentry_enabled
- Enable Clientside Sentry
- .help-block
- Sentry can also be used for reporting and logging clientside exceptions.
- %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
-
- .form-group
- = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :clientside_sentry_dsn, class: 'form-control'
-
- %fieldset
- %legend Repository Storage
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :hashed_storage_enabled do
- = f.check_box :hashed_storage_enabled
- Create new projects using hashed storage paths
- .help-block
- Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
- repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
- %em (EXPERIMENTAL)
- .form-group
- = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
- .col-sm-10
- = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
- {include_hidden: false}, multiple: true, class: 'form-control'
- .help-block
- Manage repository storage paths. Learn more in the
- = succeed "." do
- = link_to "repository storages documentation", help_page_path("administration/repository_storages")
-
- %fieldset
- %legend Git Storage Circuitbreaker settings
- .form-group
- = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_check_interval, class: 'form-control'
- .help-block
- = circuitbreaker_check_interval_help_text
- .form-group
- = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_access_retries, class: 'form-control'
- .help-block
- = circuitbreaker_access_retries_help_text
- .form-group
- = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
- .help-block
- = circuitbreaker_storage_timeout_help_text
- .form-group
- = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
- .help-block
- = circuitbreaker_failure_count_help_text
- .form-group
- = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
- .help-block
- = circuitbreaker_failure_reset_time_help_text
-
- %fieldset
- %legend Repository Checks
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :repository_checks_enabled do
- = f.check_box :repository_checks_enabled
- Enable Repository Checks
- .help-block
- GitLab will periodically run
- %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
- in all project and wiki repositories to look for silent disk corruption issues.
- .form-group
- .col-sm-offset-2.col-sm-10
- = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
- .help-block
- If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
-
- - if koding_enabled?
- %fieldset
- %legend Koding
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :koding_enabled do
- = f.check_box :koding_enabled
- Enable Koding
- .help-block
- Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
- .form-group
- = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .help-block
- Koding has integration enabled out of the box for the
- %strong gitlab
- team, and you need to provide that team's URL here. Learn more in the
- = succeed "." do
- = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
-
- %fieldset
- %legend PlantUML
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :plantuml_enabled do
- = f.check_box :plantuml_enabled
- Enable PlantUML
- .form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .help-block
- Allow rendering of
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
-
- %fieldset
- %legend#usage-statistics Usage statistics
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Enable version check
- .help-block
- GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
- about what information is shared with GitLab Inc.
- .form-group
- .col-sm-offset-2.col-sm-10
- - can_be_configured = @application_setting.usage_ping_can_be_configured?
- .checkbox
- = f.label :usage_ping_enabled do
- = f.check_box :usage_ping_enabled, disabled: !can_be_configured
- Enable usage ping
- .help-block
- - if can_be_configured
- To help improve GitLab and its user experience, GitLab will
- periodically collect usage information.
- = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
- about what information is shared with GitLab Inc. Visit
- = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
- to see the JSON payload sent.
- - else
- The usage ping is disabled, and cannot be configured through this
- form. For more information, see the documentation on
- = succeed '.' do
- = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
-
- %fieldset
- %legend Email
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- Some email servers do not support overriding the email sender name.
- Enable this option to include the name of the author of the issue,
- merge request or comment in the email body instead.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :html_emails_enabled do
- = f.check_box :html_emails_enabled
- Enable HTML emails
- .help-block
- By default GitLab sends emails in HTML and plain text formats so mail
- clients can choose what format to use. Disable this option if you only
- want to send emails in plain text format.
- %fieldset
- %legend Automatic Git repository housekeeping
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :housekeeping_enabled do
- = f.check_box :housekeeping_enabled
- Enable automatic repository housekeeping (git repack, git gc)
- .help-block
- If you keep automatic housekeeping disabled for a long time Git
- repository access on your GitLab server will become slower and your
- repositories will use more disk space. We recommend to always leave
- this enabled.
- .checkbox
- = f.label :housekeeping_bitmaps_enabled do
- = f.check_box :housekeeping_bitmaps_enabled
- Enable Git pack file bitmap creation
- .help-block
- Creating pack file bitmaps makes housekeeping take a little longer but
- bitmaps should accelerate 'git clone' performance.
- .form-group
- = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
- .help-block
- Number of Git pushes after which an incremental 'git repack' is run.
- .form-group
- = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_full_repack_period, class: 'form-control'
- .help-block
- Number of Git pushes after which a full 'git repack' is run.
- .form-group
- = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :housekeeping_gc_period, class: 'form-control'
- .help-block
- Number of Git pushes after which 'git gc' is run.
-
- %fieldset
- %legend Gitaly Timeouts
- .form-group
- = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_default, class: 'form-control'
- .help-block
- Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
- for git fetch/push operations or Sidekiq jobs.
- .form-group
- = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_fast, class: 'form-control'
- .help-block
- Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
- If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
- can help maintain the stability of the GitLab instance.
- .form-group
- = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :gitaly_timeout_medium, class: 'form-control'
- .help-block
- Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
-
- %fieldset
- %legend Web terminal
- .form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :terminal_max_session_time, class: 'form-control'
- .help-block
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
-
- %fieldset
- %legend Real-time features
- .form-group
- = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :polling_interval_multiplier, class: 'form-control'
- .help-block
- Change this value to influence how frequently the GitLab UI polls for updates.
- If you set the value to 2 all polling intervals are multiplied
- by 2, which means that polling happens half as frequently.
- The multiplier can also have a decimal value.
- The default value (1) is a reasonable choice for the majority of GitLab
- installations. Set to 0 to completely disable polling.
- = link_to icon('question-circle'), help_page_path('administration/polling')
-
- %fieldset
- %legend Performance optimization
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :authorized_keys_enabled do
- = f.check_box :authorized_keys_enabled
- Write to "authorized_keys" file
- .help-block
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
-
- %fieldset
- %legend User and IP Rate Limits
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :throttle_unauthenticated_enabled do
- = f.check_box :throttle_unauthenticated_enabled
- Enable unauthenticated request rate limit
- %span.help-block
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :throttle_authenticated_api_enabled do
- = f.check_box :throttle_authenticated_api_enabled
- Enable authenticated API request rate limit
- %span.help-block
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :throttle_authenticated_web_enabled do
- = f.check_box :throttle_authenticated_web_enabled
- Enable authenticated web request rate limit
- %span.help-block
- Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
-
- %fieldset
- %legend Outbound requests
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :allow_local_requests_from_hooks_and_services do
- = f.check_box :allow_local_requests_from_hooks_and_services
- Allow requests to the local network from hooks and services
-
- .form-actions
- = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
new file mode 100644
index 00000000000..4acc5b3a0c5
--- /dev/null
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -0,0 +1,27 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_default, class: 'form-control'
+ .help-block
+ Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+ for git fetch/push operations or Sidekiq jobs.
+ .form-group
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_fast, class: 'form-control'
+ .help-block
+ Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+ If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+ can help maintain the stability of the GitLab instance.
+ .form-group
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :gitaly_timeout_medium, class: 'form-control'
+ .help-block
+ Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
new file mode 100644
index 00000000000..3bc101ddf04
--- /dev/null
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :help_page_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :help_page_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :help_page_hide_commercial_content do
+ = f.check_box :help_page_hide_commercial_content
+ Hide marketing-related entries from help
+ .form-group
+ = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
+ %span.help-block#support_help_block Alternate support URL for help page
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
new file mode 100644
index 00000000000..a173fd38a9c
--- /dev/null
+++ b/app/views/admin/application_settings/_influx.html.haml
@@ -0,0 +1,68 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ Setup InfluxDB to measure a wide variety of statistics like the time spent
+ in running SQL queries. These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :metrics_enabled do
+ = f.check_box :metrics_enabled
+ Enable InfluxDB Metrics
+ .form-group
+ = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
+ .form-group
+ = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
+ .help-block
+ The UDP port to use for connecting to InfluxDB. InfluxDB requires that
+ your server configuration specifies a database to store data in when
+ sending messages to this port, without it metrics data will not be
+ saved.
+ .form-group
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_pool_size, class: 'form-control'
+ .help-block
+ The amount of InfluxDB connections to open. Connections are opened
+ lazily. Users using multi-threaded application servers should ensure
+ enough connections are available (at minimum the amount of application
+ server threads).
+ .form-group
+ = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_timeout, class: 'form-control'
+ .help-block
+ The amount of seconds after which an InfluxDB connection will time
+ out.
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .help-block
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .help-block
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
+ .form-group
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_packet_size, class: 'form-control'
+ .help-block
+ The amount of points to store in a single UDP packet. More points
+ results in fewer but larger UDP packets being sent.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
new file mode 100644
index 00000000000..b83ffc375d9
--- /dev/null
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -0,0 +1,54 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_unauthenticated_enabled do
+ = f.check_box :throttle_unauthenticated_enabled
+ Enable unauthenticated request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_api_enabled do
+ = f.check_box :throttle_authenticated_api_enabled
+ Enable authenticated API request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :throttle_authenticated_web_enabled do
+ = f.check_box :throttle_authenticated_web_enabled
+ Enable authenticated web request rate limit
+ %span.help-block
+ Helps reduce request volume (e.g. from crawlers or abusive bots)
+ .form-group
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+ .form-group
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
new file mode 100644
index 00000000000..17358cf775b
--- /dev/null
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -0,0 +1,24 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .help-block
+ Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
new file mode 100644
index 00000000000..44a11ddc120
--- /dev/null
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -0,0 +1,36 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sentry_enabled do
+ = f.check_box :sentry_enabled
+ Enable Sentry
+ .help-block
+ %p This setting requires a restart to take effect.
+ Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
+
+ .form-group
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :sentry_dsn, class: 'form-control'
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :clientside_sentry_enabled do
+ = f.check_box :clientside_sentry_enabled
+ Enable Clientside Sentry
+ .help-block
+ Sentry can also be used for reporting and logging clientside exceptions.
+ %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
+
+ .form-group
+ = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :clientside_sentry_dsn, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
new file mode 100644
index 00000000000..d10f609006d
--- /dev/null
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -0,0 +1,12 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :allow_local_requests_from_hooks_and_services do
+ = f.check_box :allow_local_requests_from_hooks_and_services
+ Allow requests to the local network from hooks and services
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
new file mode 100644
index 00000000000..b28ecf9a039
--- /dev/null
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block 0 for unlimited
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :pages_domain_verification_enabled do
+ = f.check_box :pages_domain_verification_enabled
+ Require users to prove ownership of custom domains
+ .help-block
+ Domain verification is an essential security measure for public GitLab
+ sites. Users are required to demonstrate they control a domain before
+ it is enabled
+ = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
new file mode 100644
index 00000000000..01d5a31aa9f
--- /dev/null
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -0,0 +1,19 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :authorized_keys_enabled do
+ = f.check_box :authorized_keys_enabled
+ Write to "authorized_keys" file
+ .help-block
+ By default, we write to the "authorized_keys" file to support Git
+ over SSH without additional configuration. GitLab can be optimized
+ to authenticate SSH keys via the database file. Only uncheck this
+ if you have configured your OpenSSH server to use the
+ AuthorizedKeysCommand. Click on the help icon for more details.
+ = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
new file mode 100644
index 00000000000..5344f030c97
--- /dev/null
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :performance_bar_enabled do
+ = f.check_box :performance_bar_enabled
+ Enable the Performance Bar
+ .form-group
+ = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
new file mode 100644
index 00000000000..56764b3fb81
--- /dev/null
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -0,0 +1,20 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :plantuml_enabled do
+ = f.check_box :plantuml_enabled
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .help-block
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
new file mode 100644
index 00000000000..48745db2991
--- /dev/null
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -0,0 +1,28 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %p
+ Enable a Prometheus metrics endpoint at
+ %code= metrics_path
+ to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available
+ = link_to 'here', admin_health_check_path
+ \. This setting requires a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :prometheus_metrics_enabled do
+ = f.check_box :prometheus_metrics_enabled
+ Enable Prometheus Metrics
+ - unless Gitlab::Metrics.metrics_folder_present?
+ .help-block
+ %strong.cred WARNING:
+ Environment variable
+ %code prometheus_multiproc_dir
+ does not exist or is not pointing to a valid directory.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
new file mode 100644
index 00000000000..0a53a75119e
--- /dev/null
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -0,0 +1,19 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .help-block
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+ = link_to icon('question-circle'), help_page_path('administration/polling')
+
+ = f.submit 'Save changes', class: "btn btn-success"
+
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
new file mode 100644
index 00000000000..3451ef62458
--- /dev/null
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -0,0 +1,10 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
new file mode 100644
index 00000000000..f33769b23c2
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -0,0 +1,62 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .sub-section
+ %h4 Repository checks
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :repository_checks_enabled do
+ = f.check_box :repository_checks_enabled
+ Enable Repository Checks
+ .help-block
+ GitLab will periodically run
+ %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+ in all project and wiki repositories to look for silent disk corruption issues.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+ .help-block
+ If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+
+ .sub-section
+ %h4 Housekeeping
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :housekeeping_enabled do
+ = f.check_box :housekeeping_enabled
+ Enable automatic repository housekeeping (git repack, git gc)
+ .help-block
+ If you keep automatic housekeeping disabled for a long time Git
+ repository access on your GitLab server will become slower and your
+ repositories will use more disk space. We recommend to always leave
+ this enabled.
+ .checkbox
+ = f.label :housekeeping_bitmaps_enabled do
+ = f.check_box :housekeeping_bitmaps_enabled
+ Enable Git pack file bitmap creation
+ .help-block
+ Creating pack file bitmaps makes housekeeping take a little longer but
+ bitmaps should accelerate 'git clone' performance.
+ .form-group
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which an incremental 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which a full 'git repack' is run.
+ .form-group
+ = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :housekeeping_gc_period, class: 'form-control'
+ .help-block
+ Number of Git pushes after which 'git gc' is run.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
new file mode 100644
index 00000000000..ac31977e1a9
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -0,0 +1,58 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .sub-section
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :hashed_storage_enabled do
+ = f.check_box :hashed_storage_enabled
+ Create new projects using hashed storage paths
+ .help-block
+ Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
+ repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
+ %em (EXPERIMENTAL)
+ .form-group
+ = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
+ {include_hidden: false}, multiple: true, class: 'form-control'
+ .help-block
+ Manage repository storage paths. Learn more in the
+ = succeed "." do
+ = link_to "repository storages documentation", help_page_path("administration/repository_storages")
+ .sub-section
+ %h4 Circuit breaker
+ .form-group
+ = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_check_interval, class: 'form-control'
+ .help-block
+ = circuitbreaker_check_interval_help_text
+ .form-group
+ = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_access_retries, class: 'form-control'
+ .help-block
+ = circuitbreaker_access_retries_help_text
+ .form-group
+ = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+ .help-block
+ = circuitbreaker_storage_timeout_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+ .help-block
+ = circuitbreaker_failure_count_help_text
+ .form-group
+ = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+ .help-block
+ = circuitbreaker_failure_reset_time_help_text
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
new file mode 100644
index 00000000000..864e64b5fa9
--- /dev/null
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -0,0 +1,59 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_web do
+ = f.check_box :password_authentication_enabled_for_web
+ Password authentication enabled for web interface
+ .help-block
+ When disabled, an external authentication provider must be used.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :password_authentication_enabled_for_git do
+ = f.check_box :password_authentication_enabled_for_git
+ Password authentication enabled for Git over HTTP(S)
+ .help-block
+ When disabled, a Personal Access Token
+ - if Gitlab::Auth::LDAP::Config.enabled?
+ or LDAP password
+ must be used to authenticate.
+ - if omniauth_enabled? && button_based_providers.any?
+ .form-group
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
+ .col-sm-10
+ .btn-group{ data: { toggle: 'buttons' } }
+ - oauth_providers_checkboxes.each do |source|
+ = source
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ Require all users to setup Two-factor authentication
+ .form-group
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
+ .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-group
+ = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
+ %span.help-block#home_help_block We will redirect non-logged in users to this page
+ .form-group
+ = f.label :after_sign_out_path, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
+ %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ .form-group
+ = f.label :sign_in_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :sign_in_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
new file mode 100644
index 00000000000..85f311dd894
--- /dev/null
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -0,0 +1,58 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :signup_enabled do
+ = f.check_box :signup_enabled
+ Sign-up enabled
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :send_user_confirmation_email do
+ = f.check_box :send_user_confirmation_email
+ Send confirmation email on sign-up
+ .form-group
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :domain_blacklist_enabled do
+ = f.check_box :domain_blacklist_enabled
+ Enable domain blacklist for sign ups
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .radio
+ = label_tag :blacklist_type_file do
+ = radio_button_tag :blacklist_type, :file
+ .option-title
+ Upload blacklist file
+ .radio
+ = label_tag :blacklist_type_raw do
+ = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
+ .option-title
+ Enter blacklist manually
+ .form-group.blacklist-file
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ .form-group.blacklist-raw
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+
+ .form-group
+ = f.label :after_sign_up_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
new file mode 100644
index 00000000000..25e89097dfe
--- /dev/null
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -0,0 +1,65 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :recaptcha_enabled do
+ = f.check_box :recaptcha_enabled
+ Enable reCAPTCHA
+ %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .help-block
+ Generate site and private keys at
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :akismet_enabled do
+ = f.check_box :akismet_enabled
+ Enable Akismet
+ %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+ .form-group
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :akismet_api_key, class: 'form-control'
+ .help-block
+ Generate API key at
+ %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :unique_ips_limit_enabled do
+ = f.check_box :unique_ips_limit_enabled
+ Limit sign in from multiple ips
+ %span.help-block#unique_ip_help_block
+ Helps prevent malicious users hide their activity
+
+ .form-group
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+ .help-block
+ Maximum number of unique IPs per user
+
+ .form-group
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+ .help-block
+ How many seconds an IP will be counted towards the limit
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
new file mode 100644
index 00000000000..36d8838803f
--- /dev/null
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -0,0 +1,13 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .help-block
+ Maximum time for web terminal websocket connection (in seconds).
+ 0 for unlimited.
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
new file mode 100644
index 00000000000..7684e2cfdd1
--- /dev/null
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -0,0 +1,37 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Enable version check
+ .help-block
+ GitLab will inform you if a new version is available.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+ about what information is shared with GitLab Inc.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ - can_be_configured = @application_setting.usage_ping_can_be_configured?
+ .checkbox
+ = f.label :usage_ping_enabled do
+ = f.check_box :usage_ping_enabled, disabled: !can_be_configured
+ Enable usage ping
+ .help-block
+ - if can_be_configured
+ To help improve GitLab and its user experience, GitLab will
+ periodically collect usage information.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ about what information is shared with GitLab Inc. Visit
+ = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
+ - else
+ The usage ping is disabled, and cannot be configured through this
+ form. For more information, see the documentation on
+ = succeed '.' do
+ = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+
+ = f.submit 'Save changes', class: "btn btn-success"
+
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
new file mode 100644
index 00000000000..cbc779548f6
--- /dev/null
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -0,0 +1,66 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :default_branch_protection, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
+ .form-group.visibility-level-setting
+ = f.label :default_project_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
+ .form-group.visibility-level-setting
+ = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
+ .form-group.visibility-level-setting
+ = f.label :default_group_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
+ .form-group
+ = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
+ .col-sm-10
+ - checkbox_name = 'application_setting[restricted_visibility_levels][]'
+ = hidden_field_tag(checkbox_name)
+ - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
+ .checkbox
+ = level
+ %span.help-block#restricted-visibility-help
+ Selected levels cannot be used by non-admin users for projects or snippets.
+ If the public level is restricted, user profiles are only visible to logged in users.
+ .form-group
+ = f.label :import_sources, class: 'control-label col-sm-2'
+ .col-sm-10
+ - import_sources_checkboxes('import-sources-help').each do |source|
+ .checkbox= source
+ %span.help-block#import-sources-help
+ Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
+ = link_to "(?)", help_page_path("integration/github")
+ , Bitbucket
+ = link_to "(?)", help_page_path("integration/bitbucket")
+ and GitLab.com
+ = link_to "(?)", help_page_path("integration/gitlab")
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :project_export_enabled do
+ = f.check_box :project_export_enabled
+ Project export enabled
+
+ .form-group
+ %label.control-label.col-sm-2 Enabled Git access protocols
+ .col-sm-10
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %span.help-block#clone-protocol-help
+ Allow only the selected protocols to be used for Git access.
+
+ - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
+ - field_name = :"#{type}_key_restriction"
+ .form-group
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index ecc46d86afe..caaa93aa1e2 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,5 +1,304 @@
+- breadcrumb_title "Settings"
- page_title "Settings"
+- @content_class = "limit-container-width" unless fluid_layout
+- expanded = Rails.env.test?
-%h3.page-title Settings
-%hr
-= render 'form'
+%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Visibility and access controls')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
+ .settings-content
+ = render 'visibility_and_access'
+
+%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Account and limit settings')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Session expiration, projects limit and attachment size.')
+ .settings-content
+ = render 'account_and_limit'
+
+%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Sign-up restrictions')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure the way a user creates a new account.')
+ .settings-content
+ = render 'signup'
+
+%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Sign-in restrictions')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
+ .settings-content
+ = render 'signin'
+
+%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Help page')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Help page text and support page url.')
+ .settings-content
+ = render 'help_page'
+
+%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Pages')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Size and domain settings for static websites')
+ .settings-content
+ = render 'pages'
+
+%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Continuous Integration and Deployment')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Auto DevOps, runners and job artifacts')
+ .settings-content
+ = render 'ci_cd'
+
+%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Metrics - Influx')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable and configure InfluxDB metrics.')
+ .settings-content
+ = render 'influx'
+
+%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Metrics - Prometheus')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable and configure Prometheus metrics.')
+ .settings-content
+ = render 'prometheus'
+
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Profiling - Performance bar')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable the Performance Bar for a given group.')
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+ .settings-content
+ = render 'performance_bar'
+
+%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Background jobs')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure Sidekiq job throttling.')
+ .settings-content
+ = render 'background_jobs'
+
+%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Spam and Anti-bot Protection')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable reCAPTCHA or Akismet and set IP limits.')
+ .settings-content
+ = render 'spam'
+
+%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Abuse reports')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set notification email for abuse reports.')
+ .settings-content
+ = render 'abuse'
+
+%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Error Reporting and Logging')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable Sentry for error reporting and logging.')
+ .settings-content
+ = render 'logging'
+
+%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Repository storage')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure storage path and circuit breaker settings.')
+ .settings-content
+ = render 'repository_storage'
+
+%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Repository maintenance')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure automatic git checks and housekeeping on repositories.')
+ .settings-content
+ = render 'repository_check'
+
+- if Gitlab.config.registry.enabled
+ %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Container Registry')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Various container registry settings.')
+ .settings-content
+ = render 'registry'
+
+- if koding_enabled?
+ %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Koding')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Online IDE integration settings.')
+ .settings-content
+ = render 'koding'
+
+%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('PlantUML')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
+ .settings-content
+ = render 'plantuml'
+
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) }
+ .settings-header#usage-statistics
+ %h4
+ = _('Usage statistics')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable or disable version check and usage ping.')
+ .settings-content
+ = render 'usage'
+
+%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Email')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Various email settings.')
+ .settings-content
+ = render 'email'
+
+%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Gitaly')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure Gitaly timeouts.')
+ .settings-content
+ = render 'gitaly'
+
+%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Web terminal')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Set max session time for web terminal.')
+ .settings-content
+ = render 'terminal'
+
+%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Real-time features')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Change this value to influence how frequently the GitLab UI polls for updates.')
+ .settings-content
+ = render 'realtime'
+
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Performance optimization')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Various settings that affect GitLab performance.')
+ .settings-content
+ = render 'performance'
+
+%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('User and IP Rate Limits')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure limits for web and API requests.')
+ .settings-content
+ = render 'ip_limits'
+
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Outbound requests')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Allow requests to the local network from hooks and services.')
+ .settings-content
+ = render 'outbound'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index c02ddafe108..c47b8a88f56 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -62,12 +62,16 @@
= link_to @project.ssh_url_to_repo, project_path(@project)
- if @project.repository.exists?
%li
- %span.light fs:
+ %span.light Gitaly storage name:
%strong
- = @project.repository.path_to_repo
+ = @project.repository.storage
+ %li
+ %span.light Gitaly relative path:
+ %strong
+ = @project.repository.relative_path
%li
- %span.light Storage:
+ %span.light Storage used:
%strong= storage_counter(@project.statistics.storage_size)
(
= storage_counter(@project.statistics.repository_size)
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 3c0881caa06..22f149d1caa 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,27 +1,9 @@
-- page_title "CI Lint"
-- page_description "Validate your GitLab CI configuration file"
-- content_for :library_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
-
-%h2 Check your .gitlab-ci.yml
-
-.ci-linter
- .row
- = form_tag ci_lint_path, method: :post do
- .form-group
- .col-sm-12
- .file-holder
- .js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
- #ci-editor.ci-editor= @content
- = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
- .col-sm-12
- .pull-left.prepend-top-10
- = submit_tag('Validate', class: 'btn btn-success submit-yml')
- .pull-right.prepend-top-10
- = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
-
- .row.prepend-top-20
- .col-sm-12
- .results.ci-template
- = render partial: 'create' if defined?(@status)
+.row.empty-state
+ .col-xs-12
+ .svg-content
+ = image_tag 'illustrations/feature_moved.svg'
+ .col-xs-12
+ .text-content.text-center
+ %h4= _("GitLab CI Linter has been moved")
+ %p
+ = _("To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button.")
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 15201780451..440623b34f5 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -10,7 +10,7 @@
- id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
- key_input_name = "#{form_field}[variables_attributes][][key]"
-- value_input_name = "#{form_field}[variables_attributes][][value]"
+- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
- protected_input_name = "#{form_field}[variables_attributes][][protected]"
%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
@@ -43,7 +43,5 @@
%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')
- -# EE-specific start
- -# EE-specific end
%button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml
index 6693e6f90e8..af518b5b583 100644
--- a/app/views/email_rejection_mailer/rejection.text.haml
+++ b/app/views/email_rejection_mailer/rejection.text.haml
@@ -1,4 +1,3 @@
Unfortunately, your email message to GitLab could not be processed.
-
-
+\
= @reason
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 7f9486d08d9..8e1dea4afc1 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 54ef51b30e3..c63cf2b31cb 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -22,9 +22,6 @@
= text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
= submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
- -# EE-specific start
- -# EE-specific end
-
- unless github_import_configured?
%hr
%p
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 257f7326409..6513b719199 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html.devise-layout-html
+%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
@@ -16,7 +16,7 @@
%h1
= brand_title
= brand_image
- - if brand_item&.description?
+ - if current_appearance&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 8718bb3db1a..adf90cb7667 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en" }
+%html{ lang: "en", class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless
= render "layouts/header/empty"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 059571f795f..5c90d13420f 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -80,14 +80,6 @@
= link_to charts_project_graph_path(@project, current_ref) do
#{ _('Charts') }
- - if project_nav_tab? :container_registry
- = nav_link(controller: %w[projects/registry/repositories]) do
- = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
- .nav-icon-container
- = sprite_icon('disk')
- %span.nav-item-name
- Registry
-
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), class: 'shortcuts-issues' do
@@ -231,6 +223,14 @@
%span
Charts
+ - if project_nav_tab? :container_registry
+ = nav_link(controller: %w[projects/registry/repositories]) do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
+ .nav-icon-container
+ = sprite_icon('disk')
+ %span.nav-item-name
+ Registry
+
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
new file mode 100644
index 00000000000..4c507c08ed7
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -0,0 +1,26 @@
+%h3
+ = @updated_by_user.name
+ pushed new commits to merge request
+ = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
+
+- if @existing_commits.any?
+ - count = @existing_commits.size
+ %ul
+ %li
+ - if count.one?
+ - commit_id = @existing_commits.first[:short_id]
+ = link_to(commit_id, project_commit_url(@merge_request.target_project, commit_id))
+ - else
+ = link_to(project_compare_url(@merge_request.target_project, from: @existing_commits.first[:short_id], to: @existing_commits.last[:short_id])) do
+ #{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}
+ = precede '&nbsp;- ' do
+ - commits_text = "#{count} commit".pluralize(count)
+ #{commits_text} from branch `#{@merge_request.target_branch}`
+
+- if @new_commits.any?
+ %ul
+ - @new_commits.each do |commit|
+ %li
+ = link_to(commit[:short_id], project_commit_url(@merge_request.target_project, commit[:short_id]))
+ = precede ' - ' do
+ #{commit[:title]}
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
new file mode 100644
index 00000000000..553f771f1a6
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -0,0 +1,13 @@
+#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference}
+\
+#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
+\
+- if @existing_commits.any?
+ - count = @existing_commits.size
+ - commits_id = count.one? ? @existing_commits.first[:short_id] : "#{@existing_commits.first[:short_id]}...#{@existing_commits.last[:short_id]}"
+ - commits_text = "#{count} commit".pluralize(count)
+
+ * #{commits_id} - #{commits_text} from branch `#{@merge_request.target_branch}`
+\
+- @new_commits.each do |commit|
+ * #{commit[:short_id]} - #{raw commit[:title]}
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 5dfe973f33c..1e7d9444986 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -7,7 +7,7 @@
.settings-header
%h4
Export project
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
@@ -21,11 +21,11 @@
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %li LFS objects
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
- %li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index 30bf1384b22..30bf1384b22 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
new file mode 100644
index 00000000000..6ca8152183d
--- /dev/null
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -0,0 +1,27 @@
+- page_title "CI Lint"
+- page_description "Validate your GitLab CI configuration file"
+- content_for :library_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+
+%h2 Check your .gitlab-ci.yml
+
+.project-ci-linter
+ .row
+ = form_tag project_ci_lint_path(@project), method: :post do
+ .form-group
+ .col-sm-12
+ .file-holder
+ .js-file-title.file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ .col-sm-12
+ .pull-left.prepend-top-10
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
+ .pull-right.prepend-top-10
+ = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
+
+ .row.prepend-top-20
+ .col-sm-12
+ .results.project-ci-template
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2ee0eafcf1a..4c510293204 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -31,7 +31,7 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
@@ -43,7 +43,7 @@
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
diff --git a/app/views/projects/clusters/user/_header.html.haml b/app/views/projects/clusters/user/_header.html.haml
index 04c7ce96a4b..37f6a788518 100644
--- a/app/views/projects/clusters/user/_header.html.haml
+++ b/app/views/projects/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 461129a3e0e..74c5317428c 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -49,10 +49,10 @@
.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
- = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
+ = markdown_field(@commit, :title)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
+ = preserve(markdown_field(@commit, :description))
.info-well
.well-segment.branch-info
diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder
index 50f7e7a3a33..640b5ecf99e 100644
--- a/app/views/projects/commits/_commit.atom.builder
+++ b/app/views/projects/commits/_commit.atom.builder
@@ -10,5 +10,5 @@ xml.entry do
xml.email commit.author_email
end
- xml.summary markdown(commit.description, pipeline: :single_line), type: 'html'
+ xml.summary markdown_field(commit, :description), type: 'html'
end
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 75dd4c9ae15..7dd8dc28e5b 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
- %button.btn.js-settings-toggle.qa-expand-deploy-keys
+ %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a96485ab155..99eeb9551e3 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -8,7 +8,7 @@
.settings-header
%h4
General project settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
@@ -64,7 +64,7 @@
.settings-header
%h4
Permissions
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
@@ -79,7 +79,7 @@
.settings-header
%h4
Merge request settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
@@ -94,7 +94,7 @@
.settings-header
%h4
Advanced settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 8a36fada389..b15fe514a08 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index e779473c239..ecf186e3dc8 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -35,7 +35,7 @@
= link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download
- - if @build.artifacts_metadata?
+ - if @build.browsable_artifacts?
= link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do
Browse
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 849c273db8c..fa27ded7cc2 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -111,4 +111,4 @@
.js-build-options{ data: javascript_build_options }
-#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } }
+#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } }
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b423888c875..5ec219fdf00 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -30,6 +30,7 @@
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal',
milestone_title: @milestone.title,
+ group_name: @project.group.name,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 8cdb0a6aff4..b66e0559603 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,8 +18,6 @@
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
- -# EE-specific start
- -# EE-specific end
.md
= brand_new_project_guidelines
%p
@@ -43,8 +41,6 @@
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Import project
%span.visible-xs Import
- -# EE-specific start
- -# EE-specific end
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -110,10 +106,6 @@
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
-
- -# EE-specific start
- -# EE-specific end
-
.save-project-loader.hide
.center
%h2
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index ba5845877e5..14d880028c7 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,3 +1,5 @@
+- breadcrumb_title _("Details")
+
%h2
%i.fa.fa-warning
#{ _('No repository') }
@@ -10,7 +12,7 @@
.no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do
- #{ _('Create empty bare repository') }
+ #{ _('Create empty repository') }
%strong.prepend-left-10.append-right-10 or
@@ -19,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 5a397c9d3c7..e49163880c7 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -8,3 +8,5 @@
= render 'form', { f: f }
.form-actions
= f.submit 'Create New Domain', class: "btn btn-save"
+ .pull-right
+ = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 3e6b3346787..c0ee81fe28d 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -10,6 +10,6 @@
"no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
- "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
+ "ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project),
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 2a0704bc7af..a09c13176c3 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -2,7 +2,7 @@
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
- Protected branch (#{@protected_branches.size})
+ Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
- else
@@ -16,7 +16,7 @@
%col
%thead
%tr
- %th Protected branch (#{@protected_branches.size})
+ %th Protected branch (#{@protected_branches_count})
%th Last commit
%th Allowed to merge
%th Allowed to push
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index e662b877fbb..55d87c35a80 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 24baf1cfc89..c33723d8072 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Tags
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 3f42ae58438..02908e16dc5 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -2,7 +2,7 @@
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
- Protected tag (#{@protected_tags.size})
+ Protected tag (#{@protected_tags_count})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
- else
@@ -17,7 +17,7 @@
%col
%thead
%tr
- %th Protected tag (#{@protected_tags.size})
+ %th Protected tag (#{@protected_tags_count})
%th Last commit
%th Allowed to create
- if can_admin_project
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 49c90869146..6a681736b6f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -40,6 +40,12 @@
.col-sm-10
= f.text_field :description, class: 'form-control'
.form-group
+ = label_tag :maximum_timeout_human_readable, class: 'control-label' do
+ Maximum job timeout
+ .col-sm-10
+ = f.text_field :maximum_timeout_human_readable, class: 'form-control'
+ .help-block This timeout will take precedence when lower than Project-defined timeout
+ .form-group
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 4e57f5f844d..f33e7e25b68 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -56,6 +56,9 @@
%td Description
%td= @runner.description
%tr
+ %td Maximum job timeout
+ %td= @runner.maximum_timeout_human_readable
+ %tr
%td Last contact
%td
- if @runner.contacted_at
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index e8028059487..e8028059487 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 646c01c0989..20868f9ba5d 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -1,6 +1,7 @@
.row.prepend-top-default
.col-lg-12
- = form_for @project, url: project_pipelines_settings_path(@project) do |f|
+ = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
+ = form_errors(@project)
%fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
@@ -73,10 +74,10 @@
%hr
.form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
+ = f.text_field :build_timeout_human_readable, class: 'form-control'
%p.help-block
- Per job in minutes. If a job passes this threshold, it will be marked as failed
+ Per job. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
@@ -151,10 +152,13 @@
%li
excoveralls (Elixir) -
%code \[TOTAL\]\s+(\d+\.\d+)%
+ %li
+ JaCoCo (Java/Kotlin)
+ %code Total.*?([0-9]{1,3})%
= f.submit 'Save changes', class: "btn btn-save"
%hr
.row.prepend-top-default
- = render partial: 'projects/pipelines_settings/badge', collection: @badges
+ = render partial: 'badge', collection: @badges
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 756f31f91d9..09268c9943b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -3,23 +3,24 @@
- page_title "CI / CD"
- expanded = Rails.env.test?
+- general_expanded = @project.errors.empty? ? expanded : true
-%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
General pipelines settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content
- = render 'projects/pipelines_settings/show'
+ = render 'form'
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Runners settings
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
@@ -31,7 +32,7 @@
%h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p.append-bottom-0
= render "ci/variables/content"
@@ -42,7 +43,7 @@
.settings-header
%h4
Pipeline triggers
- %button.btn.js-settings-toggle
+ %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fa281327eb7..94331a16abd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
- show_auto_devops_callout = show_auto_devops_callout?(@project)
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 5eaaa1448d5..3806ead6c87 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -17,6 +17,3 @@
= import_will_timeout_message(ci_cd_only)
%li
= import_svn_message(ci_cd_only)
-
--# EE-specific start
--# EE-specific end
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 5afbc78df53..56403907844 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -55,6 +55,7 @@
label_title: label.title,
label_color: label.color,
label_text_color: label.text_color,
+ group_name: label.project.group.name,
target: '#promote-label-modal',
container: 'body',
toggle: 'modal' } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index adaddda13eb..975b9cb4729 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -84,7 +84,7 @@
= dropdown_content do
.js-due-date-calendar
- - if @labels && @labels.any?
+ - 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" } }
@@ -107,7 +107,7 @@
- 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: (project_labels_path(@project, :json) if @project) } }
+ %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(false) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 5926867e2d7..ac494814f55 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -56,6 +56,7 @@
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
+ group_name: @project.group.name,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 4539c745f9a..797ff034bb2 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,4 +1,6 @@
-- page_title milestone.title, "Milestones"
+- page_title milestone.title
+- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+
- group = local_assigns[:group]
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
@@ -17,7 +19,7 @@
Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date
%span.creator
- &middot;
+ &nbsp;&middot;
= milestone_date_range(milestone)
- if group
.pull-right
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 71c0d740bc8..725bf916592 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -24,21 +24,20 @@
-# DiffNote
= f.hidden_field :position
- .discussion-form-container
- = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
- = render 'projects/zen', f: f,
- attr: :note,
- classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here...",
- supports_quick_actions: supports_quick_actions,
- supports_autocomplete: supports_autocomplete
- = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
- .error-alert
-
- .note-form-actions.clearfix
- = render partial: 'shared/notes/comment_button'
-
- = yield(:note_actions)
-
- %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
- Discard draft
+ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
+ = render 'projects/zen', f: f,
+ attr: :note,
+ classes: 'note-textarea js-note-text',
+ placeholder: "Write a comment or drag your files here...",
+ supports_quick_actions: supports_quick_actions,
+ supports_autocomplete: supports_autocomplete
+ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
+ .error-alert
+
+ .note-form-actions.clearfix
+ = render partial: 'shared/notes/comment_button'
+
+ = yield(:note_actions)
+
+ %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } }
+ Discard draft
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ad4d39b4aa1..d36ca032558 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -33,6 +33,13 @@
%p.light
This URL will be triggered when someone adds a comment
%li
+ = form.check_box :confidential_note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :confidential_note_events, class: 'list-label' do
+ %strong Confidential Comments
+ %p.light
+ This URL will be triggered when someone adds a comment on a confidential issue
+ %li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= form.label :issues_events, class: 'list-label' do
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f65e8385ac8..9a11cdb121e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -39,6 +39,10 @@
- github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository
+- object_storage_upload
+- object_storage:object_storage_background_move
+- object_storage:object_storage_migrate_uploads
+
- pipeline_cache:expire_job_cache
- pipeline_cache:expire_pipeline_cache
- pipeline_creation:create_pipeline
diff --git a/app/workers/concerns/object_storage_queue.rb b/app/workers/concerns/object_storage_queue.rb
new file mode 100644
index 00000000000..a80f473a6d4
--- /dev/null
+++ b/app/workers/concerns/object_storage_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various GitLab ObjectStorage workers.
+module ObjectStorageQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :object_storage
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 55fb817ca6e..be4203bc7ad 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -28,16 +28,17 @@ class GitGarbageCollectWorker
task = task.to_sym
cmd = command(task)
- repo_path = project.repository.path_to_repo
- description = "'#{cmd.join(' ')}' in #{repo_path}"
-
- Gitlab::GitLogger.info(description)
gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled|
if is_enabled
gitaly_call(task, project.repository.raw_repository)
else
+ repo_path = project.repository.path_to_repo
+ description = "'#{cmd.join(' ')}' in #{repo_path}"
+ Gitlab::GitLogger.info(description)
+
output, status = Gitlab::Popen.popen(cmd, repo_path)
+
Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
end
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 67c54fbf10e..b925741934a 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note)
+ NotificationService.new.new_note(note) if note.can_create_notification?
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
new file mode 100644
index 00000000000..9c4d72e0ecf
--- /dev/null
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -0,0 +1,29 @@
+module ObjectStorage
+ class BackgroundMoveWorker
+ include ApplicationWorker
+ include ObjectStorageQueue
+
+ sidekiq_options retry: 5
+
+ def perform(uploader_class_name, subject_class_name, file_field, subject_id)
+ uploader_class = uploader_class_name.constantize
+ subject_class = subject_class_name.constantize
+
+ return unless uploader_class < ObjectStorage::Concern
+ return unless uploader_class.object_store_enabled?
+ return unless uploader_class.background_upload_enabled?
+
+ subject = subject_class.find(subject_id)
+ uploader = build_uploader(subject, file_field&.to_sym)
+ uploader.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ def build_uploader(subject, mount_point)
+ case subject
+ when Upload then subject.build_uploader(mount_point)
+ else
+ subject.send(mount_point) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
new file mode 100644
index 00000000000..a6b2c251254
--- /dev/null
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/LineLength
+# rubocop:disable Style/Documentation
+
+module ObjectStorage
+ class MigrateUploadsWorker
+ include ApplicationWorker
+ include ObjectStorageQueue
+
+ SanityCheckError = Class.new(StandardError)
+
+ class Upload < ActiveRecord::Base
+ # Upper limit for foreground checksum processing
+ CHECKSUM_THRESHOLD = 100.megabytes
+
+ belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+
+ validates :size, presence: true
+ validates :path, presence: true
+ validates :model, presence: true
+ validates :uploader, presence: true
+
+ before_save :calculate_checksum!, if: :foreground_checksummable?
+ after_commit :schedule_checksum, if: :checksummable?
+
+ scope :stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
+ scope :stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
+
+ def self.hexdigest(path)
+ Digest::SHA256.file(path).hexdigest
+ end
+
+ def absolute_path
+ raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
+ return path unless relative_path?
+
+ uploader_class.absolute_path(self)
+ end
+
+ def calculate_checksum!
+ self.checksum = nil
+ return unless checksummable?
+
+ self.checksum = self.class.hexdigest(absolute_path)
+ end
+
+ def build_uploader(mounted_as = nil)
+ uploader_class.new(model, mounted_as).tap do |uploader|
+ uploader.upload = self
+ uploader.retrieve_from_store!(identifier)
+ end
+ end
+
+ def exist?
+ File.exist?(absolute_path)
+ end
+
+ def local?
+ return true if store.nil?
+
+ store == ObjectStorage::Store::LOCAL
+ end
+
+ private
+
+ def checksummable?
+ checksum.nil? && local? && exist?
+ end
+
+ def foreground_checksummable?
+ checksummable? && size <= CHECKSUM_THRESHOLD
+ end
+
+ def schedule_checksum
+ UploadChecksumWorker.perform_async(id)
+ end
+
+ def relative_path?
+ !path.start_with?('/')
+ end
+
+ def identifier
+ File.basename(path)
+ end
+
+ def uploader_class
+ Object.const_get(uploader)
+ end
+ end
+
+ class MigrationResult
+ attr_reader :upload
+ attr_accessor :error
+
+ def initialize(upload, error = nil)
+ @upload, @error = upload, error
+ end
+
+ def success?
+ error.nil?
+ end
+
+ def to_s
+ success? ? "Migration successful." : "Error while migrating #{upload.id}: #{error.message}"
+ end
+ end
+
+ module Report
+ class MigrationFailures < StandardError
+ attr_reader :errors
+
+ def initialize(errors)
+ @errors = errors
+ end
+
+ def message
+ errors.map(&:message).join("\n")
+ end
+ end
+
+ def report!(results)
+ success, failures = results.partition(&:success?)
+
+ Rails.logger.info header(success, failures)
+ Rails.logger.warn failures(failures)
+
+ raise MigrationFailures.new(failures.map(&:error)) if failures.any?
+ end
+
+ def header(success, failures)
+ "Migrated #{success.count}/#{success.count + failures.count} files."
+ end
+
+ def failures(failures)
+ failures.map { |f| "\t#{f}" }.join('\n')
+ end
+ end
+
+ include Report
+
+ def self.enqueue!(uploads, model_class, mounted_as, to_store)
+ sanity_check!(uploads, model_class, mounted_as)
+
+ perform_async(uploads.ids, model_class.to_s, mounted_as, to_store)
+ end
+
+ # We need to be sure all the uploads are for the same uploader and model type
+ # and that the mount point exists if provided.
+ #
+ def self.sanity_check!(uploads, model_class, mounted_as)
+ upload = uploads.first
+ uploader_class = upload.uploader.constantize
+ uploader_types = uploads.map(&:uploader).uniq
+ model_types = uploads.map(&:model_type).uniq
+ model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
+
+ raise(SanityCheckError, "Multiple uploaders found: #{uploader_types}") unless uploader_types.count == 1
+ raise(SanityCheckError, "Multiple model types found: #{model_types}") unless model_types.count == 1
+ raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
+ end
+
+ def perform(*args)
+ args_check!(args)
+
+ (ids, model_type, mounted_as, to_store) = args
+
+ @model_class = model_type.constantize
+ @mounted_as = mounted_as&.to_sym
+ @to_store = to_store
+
+ uploads = Upload.preload(:model).where(id: ids)
+
+ sanity_check!(uploads)
+ results = migrate(uploads)
+
+ report!(results)
+ rescue SanityCheckError => e
+ # do not retry: the job is insane
+ Rails.logger.warn "#{self.class}: Sanity check error (#{e.message})"
+ end
+
+ def sanity_check!(uploads)
+ self.class.sanity_check!(uploads, @model_class, @mounted_as)
+ end
+
+ def args_check!(args)
+ return if args.count == 4
+
+ case args.count
+ when 3 then raise SanityCheckError, "Job is missing the `model_type` argument."
+ else
+ raise SanityCheckError, "Job has wrong arguments format."
+ end
+ end
+
+ def build_uploaders(uploads)
+ uploads.map { |upload| upload.build_uploader(@mounted_as) }
+ end
+
+ def migrate(uploads)
+ build_uploaders(uploads).map(&method(:process_uploader))
+ end
+
+ def process_uploader(uploader)
+ MigrationResult.new(uploader.upload).tap do |result|
+ begin
+ uploader.migrate!(@to_store)
+ rescue => e
+ result.error = e
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
new file mode 100644
index 00000000000..5c80f34069c
--- /dev/null
+++ b/app/workers/object_storage_upload_worker.rb
@@ -0,0 +1,21 @@
+# @Deprecated - remove once the `object_storage_upload` queue is empty
+# The queue has been renamed `object_storage:object_storage_background_upload`
+#
+class ObjectStorageUploadWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: 5
+
+ def perform(uploader_class_name, subject_class_name, file_field, subject_id)
+ uploader_class = uploader_class_name.constantize
+ subject_class = subject_class_name.constantize
+
+ return unless uploader_class < ObjectStorage::Concern
+ return unless uploader_class.object_store_enabled?
+ return unless uploader_class.background_upload_enabled?
+
+ subject = subject_class.find(subject_id)
+ uploader = subject.public_send(file_field) # rubocop:disable GitlabSecurity/PublicSend
+ uploader.migrate!(ObjectStorage::Store::REMOTE)
+ end
+end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 0b502143e5d..c3d84bb0b93 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -4,11 +4,19 @@ class ProjectExportWorker
sidekiq_options retry: 3
- def perform(current_user_id, project_id, params = {})
- params = params.with_indifferent_access
+ def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
+ after_export = build!(after_export_strategy)
- ::Projects::ImportExport::ExportService.new(project, current_user, params).execute
+ ::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
+ end
+
+ private
+
+ def build!(after_export_strategy)
+ strategy_klass = after_export_strategy&.delete('klass')
+
+ Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 07584fab7c8..51fad4faf36 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -4,24 +4,47 @@ class RepositoryForkWorker
include ProjectStartImport
include ProjectImportOptions
- def perform(project_id, forked_from_repository_storage_path, source_disk_path)
- project = Project.find(project_id)
+ def perform(*args)
+ target_project_id = args.shift
+ target_project = Project.find(target_project_id)
- return unless start_fork(project)
+ # By v10.8, we should've drained the queue of all jobs using the old arguments.
+ # We can remove the else clause if we're no longer logging the message in that clause.
+ # See https://gitlab.com/gitlab-org/gitaly/issues/1110
+ if args.empty?
+ source_project = target_project.forked_from_project
+ return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project
- Gitlab::Metrics.add_event(:fork_repository,
- source_path: source_disk_path,
- target_path: project.disk_path)
+ fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
+ else
+ Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
+
+ source_repository_storage_path, source_disk_path = *args
- result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
- project.repository_storage_path, project.disk_path)
- raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
+ source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info|
+ info.legacy_disk_path == source_repository_storage_path
+ end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
- project.after_import
+ fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ end
end
private
+ def fork_repository(target_project, source_repository_storage_name, source_disk_path)
+ return unless start_fork(target_project)
+
+ Gitlab::Metrics.add_event(:fork_repository,
+ source_path: source_disk_path,
+ target_path: target_project.disk_path)
+
+ result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path,
+ target_project.repository_storage, target_project.disk_path)
+ raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result
+
+ target_project.after_import
+ end
+
def start_fork(project)
return true if start(project)